You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cloudstack.apache.org by pe...@apache.org on 2026/07/10 14:30:16 UTC

(cloudstack) branch netris-improvements created (now 0c0f22c9ad3)

This is an automated email from the ASF dual-hosted git repository.

Pearl1594 pushed a change to branch netris-improvements
in repository https://gitbox.apache.org/repos/asf/cloudstack.git


      at 0c0f22c9ad3 Allocate IP from Netris (Provider) range when IP is not specified during associate

This branch includes the following new commits:

     new 4b817d8d6e8 Netris CloudStack Integration Enhancements
     new 1618383cb7d Add a check to prevent re-adding existing ACLs
     new b66eb66623e Add check for vnet name length and cleanup vpc and vnet resources on failure of creation of allocation
     new 34a15b5bc2d Add support to the VPC response to display if redundant routers are supported
     new f73bbdd5909 Hide VPN sections and prevent enabling it when service isn't enabled in the vpc offering
     new 98ae1c4899d Fix issue with adding external node to a CKS cluster with Netris offering
     new 4ac23ee7b96 Update upgrade path to reclaim vnets of deleted netris networks, improve log and hide add upstream route for Netris routed networks
     new 0c0f22c9ad3 Allocate IP from Netris (Provider) range when IP is not specified during associate

The 8 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(cloudstack) 07/08: Update upgrade path to reclaim vnets of deleted netris networks, improve log and hide add upstream route for Netris routed networks

Posted by pe...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

Pearl1594 pushed a commit to branch netris-improvements
in repository https://gitbox.apache.org/repos/asf/cloudstack.git

commit 4ac23ee7b9663e19f3e5b22ab302523d8aa19eba
Author: Pearl Dsilva <pe...@gmail.com>
AuthorDate: Thu Jul 9 11:06:54 2026 -0400

    Update upgrade path to reclaim vnets of deleted netris networks, improve log and hide add upstream route for Netris routed networks
    
    * Update upgrade path to reclaim vnets of deleted netris networks, improve log and hide add upstream route for Netris routed networks
    
    * Apply suggestions from code review
    
    * hide upstream route banner for netris networks altogether
    
    * fix migration of netris VRs and fix reclaim vnet query
    
    ---------
    
    Co-authored-by: Pearl Dsilva <pe...@gmail.com>
    Co-authored-by: Nicolas Vazquez <ni...@gmail.com>
---
 .../engine/orchestration/NetworkOrchestrator.java           |  9 +++++++++
 .../src/main/resources/META-INF/db/schema-42200to42210.sql  |  8 ++++++++
 .../src/main/java/com/cloud/network/vpc/VpcManagerImpl.java |  2 +-
 ui/src/components/view/DetailsTab.vue                       | 13 +++++++++++++
 4 files changed, 31 insertions(+), 1 deletion(-)

diff --git a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java
index 62e084ad2bf..c082cffb1fb 100644
--- a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java
+++ b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java
@@ -2302,6 +2302,11 @@ public class NetworkOrchestrator extends ManagerBase implements NetworkOrchestra
         }
     }
 
+    private boolean isReservedForExternalProvider(Long vlanId) {
+        VlanDetailsVO vlanDetail = vlanDetailsDao.findDetail(vlanId, ApiConstants.NETRIS_DETAIL_KEY);
+        return vlanDetail != null && BooleanUtils.toBoolean(vlanDetail.getValue());
+    }
+
     /*
     Prepare All Nics for migration including the nics dynamically created and not stored in DB
     This is a temporary workaround work KVM migration
@@ -2350,6 +2355,10 @@ public class NetworkOrchestrator extends ManagerBase implements NetworkOrchestra
             final List<IPAddressVO> publicIps = _ipAddressDao.listByAssociatedNetwork(guestNetworkId, null);
             for (final IPAddressVO userIp : publicIps) {
                 final PublicIp publicIp = PublicIp.createFromAddrAndVlan(userIp, _vlanDao.findById(userIp.getVlanId()));
+                if (isReservedForExternalProvider(publicIp.getVlanId())) {
+                    logger.debug("Skipping public IP {} for migration NIC profile: managed by external network provider", publicIp.getAddress());
+                    continue;
+                }
                 final URI broadcastUri = BroadcastDomainType.Vlan.toUri(publicIp.getVlanTag());
                 final long ntwkId = publicIp.getNetworkId();
                 final Nic nic = _nicDao.findByNetworkIdInstanceIdAndBroadcastUri(ntwkId, vm.getId(),
diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42200to42210.sql b/engine/schema/src/main/resources/META-INF/db/schema-42200to42210.sql
index baa20e9f0ad..bcbe96ef7cb 100644
--- a/engine/schema/src/main/resources/META-INF/db/schema-42200to42210.sql
+++ b/engine/schema/src/main/resources/META-INF/db/schema-42200to42210.sql
@@ -59,3 +59,11 @@ UPDATE `cloud`.`configuration`
 SET value = CONCAT_WS('\n', 'Hello {{username}}!', 'You have requested to reset your password. Please click the following link to reset your password:', '{{{resetLink}}}', 'If you did not request a password reset, please ignore this email.', '', 'Regards,', 'The CloudStack Team')
 WHERE name = 'user.password.reset.mail.template'
   AND value IN (CONCAT_WS('\n', 'Hello {{username}}!', 'You have requested to reset your password. Please click the following link to reset your password:', 'http://{{{resetLink}}}', 'If you did not request a password reset, please ignore this email.', '', 'Regards,', 'The CloudStack Team'), CONCAT_WS('\n', 'Hello {{username}}!', 'You have requested to reset your password. Please click the following link to reset your password:', '{{{domainUrl}}}{{{resetLink}}}', 'If you did not request  [...]
+
+-- Reclaim VNETs belonging to Netris deleted networks
+UPDATE op_dc_vnet_alloc a
+    JOIN networks n ON n.broadcast_uri = CONCAT('netris://', a.vnet)
+    SET a.account_id = NULL,
+        a.reservation_id = NULL,
+        a.taken = NULL
+WHERE n.removed IS NOT NULL;
\ No newline at end of file
diff --git a/server/src/main/java/com/cloud/network/vpc/VpcManagerImpl.java b/server/src/main/java/com/cloud/network/vpc/VpcManagerImpl.java
index a4420fd270f..ffcd1fd00d3 100644
--- a/server/src/main/java/com/cloud/network/vpc/VpcManagerImpl.java
+++ b/server/src/main/java/com/cloud/network/vpc/VpcManagerImpl.java
@@ -3798,7 +3798,7 @@ public class VpcManagerImpl extends ManagerBase implements VpcManager, VpcProvis
             return ipAddressForVR;
         } else if (ipAddress != null) {
             if (ipAddress.isSourceNat()) {
-                throw new InvalidParameterValueException("Vpn service can not be configured on the Source NAT IP of VPC id=" + ipAddress.getVpcId());
+                throw new InvalidParameterValueException(String.format("Vpn service can not be configured on the Source NAT IP of VPC id = %s. Please acquire another IP to setup VPN", ipAddress.getVpcId()));
             }
             return ipAddress;
         }
diff --git a/ui/src/components/view/DetailsTab.vue b/ui/src/components/view/DetailsTab.vue
index 135ea7384fa..bd980924018 100644
--- a/ui/src/components/view/DetailsTab.vue
+++ b/ui/src/components/view/DetailsTab.vue
@@ -381,7 +381,17 @@ export default {
       }
       return null
     },
+    isNetrisNetwork () {
+      if (!this.resource.service) {
+        return false
+      }
+      const networkAclService = this.resource.service.find(svc => svc.name === 'NetworkACL')
+      return networkAclService && networkAclService.provider && networkAclService.provider.some(p => p.name === 'Netris')
+    },
     ip4routes () {
+      if (this.isNetrisNetwork) {
+        return null
+      }
       if (this.resource.ip4routes && this.resource.ip4routes.length > 0) {
         var routes = []
         for (var route of this.resource.ip4routes) {
@@ -392,6 +402,9 @@ export default {
       return null
     },
     ip6routes () {
+      if (this.isNetrisNetwork) {
+        return null
+      }
       if (this.resource.ip6routes && this.resource.ip6routes.length > 0) {
         var routes = []
         for (var route of this.resource.ip6routes) {


(cloudstack) 05/08: Hide VPN sections and prevent enabling it when service isn't enabled in the vpc offering

Posted by pe...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

Pearl1594 pushed a commit to branch netris-improvements
in repository https://gitbox.apache.org/repos/asf/cloudstack.git

commit f73bbdd5909b1bf17ae2a1cb62fe288f3f224b51
Author: Pearl Dsilva <pe...@gmail.com>
AuthorDate: Mon Jul 6 09:21:58 2026 -0400

    Hide VPN sections and prevent enabling it when service isn't enabled in the vpc offering
    
    * Hide VPN sections and prevent enabling it when service isn't enabled in the vpc offering
    
    * update
    
    ---------
    
    Co-authored-by: Pearl Dsilva <pe...@gmail.com>
---
 .../network/vpn/RemoteAccessVpnManagerImpl.java    |  6 +++++
 .../cloud/network/vpn/Site2SiteVpnManagerImpl.java |  3 +++
 ui/src/config/section/network.js                   |  1 +
 ui/src/views/network/VpcTab.vue                    |  4 ++--
 ui/src/views/network/VpnDetails.vue                | 28 ++++++++++++++--------
 5 files changed, 30 insertions(+), 12 deletions(-)

diff --git a/server/src/main/java/com/cloud/network/vpn/RemoteAccessVpnManagerImpl.java b/server/src/main/java/com/cloud/network/vpn/RemoteAccessVpnManagerImpl.java
index 24c2f2221d9..5e5261f657b 100644
--- a/server/src/main/java/com/cloud/network/vpn/RemoteAccessVpnManagerImpl.java
+++ b/server/src/main/java/com/cloud/network/vpn/RemoteAccessVpnManagerImpl.java
@@ -69,6 +69,7 @@ import com.cloud.network.rules.RulesManager;
 import com.cloud.network.vpc.Vpc;
 import com.cloud.network.vpc.VpcManager;
 import com.cloud.network.vpc.dao.VpcDao;
+import com.cloud.network.vpc.dao.VpcOfferingServiceMapDao;
 import com.cloud.projects.Project.ListProjectResourcesCriteria;
 import com.cloud.server.ConfigurationServer;
 import com.cloud.user.Account;
@@ -134,6 +135,8 @@ public class RemoteAccessVpnManagerImpl extends ManagerBase implements RemoteAcc
     ConfigurationServer _configServer;
     @Inject
     VpcDao _vpcDao;
+    @Inject
+    VpcOfferingServiceMapDao _vpcOfferingServiceMapDao;
 
     int _userLimit;
     int _pskLength;
@@ -243,6 +246,9 @@ public class RemoteAccessVpnManagerImpl extends ManagerBase implements RemoteAcc
                 validateIpAddressForVpnServiceOnNetwork(network, ipAddress);
             } else {
                 Vpc vpc = _vpcDao.findById(vpcId);
+                if (!_vpcOfferingServiceMapDao.areServicesSupportedByVpcOffering(vpc.getVpcOfferingId(), new Network.Service[]{Network.Service.Vpn})) {
+                    throw new InvalidParameterValueException(String.format("Vpn service is not supported for VPC: %s (%s) ", vpc.getName(), vpc.getUuid()));
+                }
                 cidr = NetUtils.getCidr(vpc.getCidr());
                 validateIpAddressForVpnServiceOnVpc(vpc, ipAddress);
             }
diff --git a/server/src/main/java/com/cloud/network/vpn/Site2SiteVpnManagerImpl.java b/server/src/main/java/com/cloud/network/vpn/Site2SiteVpnManagerImpl.java
index ad1d1f02682..34e2cf71249 100644
--- a/server/src/main/java/com/cloud/network/vpn/Site2SiteVpnManagerImpl.java
+++ b/server/src/main/java/com/cloud/network/vpn/Site2SiteVpnManagerImpl.java
@@ -144,6 +144,9 @@ public class Site2SiteVpnManagerImpl extends ManagerBase implements Site2SiteVpn
         if (vpc == null) {
             throw new InvalidParameterValueException("Invalid VPC " + vpcId + " for site to site vpn gateway creation!");
         }
+        if (!vpcOfferingServiceMapDao.areServicesSupportedByVpcOffering(vpc.getVpcOfferingId(), new Network.Service[]{Network.Service.Vpn})) {
+            throw new InvalidParameterValueException(String.format("Vpn service is not supported for VPC: %s (%s) ", vpc.getName(), vpc.getUuid()));
+        }
         Site2SiteVpnGatewayVO gws = _vpnGatewayDao.findByVpcId(vpcId);
         if (gws != null) {
             throw new InvalidParameterValueException(String.format("The VPN gateway of VPC %s already existed!", vpc));
diff --git a/ui/src/config/section/network.js b/ui/src/config/section/network.js
index acc7424c9f0..a8727811fb6 100644
--- a/ui/src/config/section/network.js
+++ b/ui/src/config/section/network.js
@@ -803,6 +803,7 @@ export default {
       }, {
         name: 'vpn',
         component: shallowRef(defineAsyncComponent(() => import('@/views/network/VpnDetails.vue'))),
+        networkServiceFilter: networkService => networkService.filter(x => x.name === 'Vpn').length > 0,
         show: (record) => { return record.issourcenat || record.virtualmachinetype === 'DomainRouter' || !record.hasrules }
       },
       {
diff --git a/ui/src/views/network/VpcTab.vue b/ui/src/views/network/VpcTab.vue
index 12e21cd8ec9..f8f3c88e78d 100644
--- a/ui/src/views/network/VpcTab.vue
+++ b/ui/src/views/network/VpcTab.vue
@@ -252,7 +252,7 @@
           </a-spin>
         </a-modal>
       </a-tab-pane>
-      <a-tab-pane :tab="$t('label.vpn.gateway')" key="vpngw" v-if="'listVpnGateways' in $store.getters.apis">
+      <a-tab-pane :tab="$t('label.vpn.gateway')" key="vpngw" v-if="'listVpnGateways' in $store.getters.apis && resource.service && resource.service.some(s => s.name === 'Vpn')">
         <a-button
           v-if="vpnGateways.length === 0"
           type="dashed"
@@ -313,7 +313,7 @@
           </a-spin>
         </a-modal>
       </a-tab-pane>
-      <a-tab-pane :tab="$t('label.vpn.connection')" key="vpnc" v-if="'listVpnConnections' in $store.getters.apis">
+      <a-tab-pane :tab="$t('label.vpn.connection')" key="vpnc" v-if="'listVpnConnections' in $store.getters.apis && resource.service && resource.service.some(s => s.name === 'Vpn')">
         <a-button
           type="dashed"
           style="width: 100%"
diff --git a/ui/src/views/network/VpnDetails.vue b/ui/src/views/network/VpnDetails.vue
index 2a75bb3c22e..3dd6c92c7f8 100644
--- a/ui/src/views/network/VpnDetails.vue
+++ b/ui/src/views/network/VpnDetails.vue
@@ -216,16 +216,24 @@ export default {
         this.$notifyError(error)
       })
       if (this.resource.vpcid) {
-        this.vpnGatewayEnabled = true
-        getAPI('listVpnGateways', {
-          vpcid: this.resource.vpcid,
-          listAll: true
-        }).then(response => {
-          const vpnGateways = response.listvpngatewaysresponse.vpngateway || []
-          for (const vpnGateway of vpnGateways) {
-            if (vpnGateway.publicip === this.resource.ipaddress) {
-              this.vpnGateway = vpnGateway
-            }
+        getAPI('listVPCs', { id: this.resource.vpcid, listAll: true }).then(vpcResponse => {
+          const vpc = vpcResponse.listvpcsresponse?.vpc?.[0]
+          if (vpc?.service?.some(s => s.name === 'Vpn')) {
+            this.vpnGatewayEnabled = true
+            getAPI('listVpnGateways', {
+              vpcid: this.resource.vpcid,
+              listAll: true
+            }).then(response => {
+              const vpnGateways = response.listvpngatewaysresponse.vpngateway || []
+              for (const vpnGateway of vpnGateways) {
+                if (vpnGateway.publicip === this.resource.ipaddress) {
+                  this.vpnGateway = vpnGateway
+                }
+              }
+            }).catch(error => {
+              console.log(error)
+              this.$notifyError(error)
+            })
           }
         }).catch(error => {
           console.log(error)


(cloudstack) 03/08: Add check for vnet name length and cleanup vpc and vnet resources on failure of creation of allocation

Posted by pe...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

Pearl1594 pushed a commit to branch netris-improvements
in repository https://gitbox.apache.org/repos/asf/cloudstack.git

commit b66eb66623ee9bfe84e57a1163c1ffa296511e66
Author: Pearl Dsilva <pe...@gmail.com>
AuthorDate: Mon Jul 6 09:16:41 2026 -0400

    Add check for vnet name length and cleanup vpc and vnet resources on failure of creation of allocation
    
    * Netris - Support NAT, PF for Secondary IP
    
    * Allow PF rules to be correctly created on secondary IP of a VM on netris
    
    * unique static nat names on netris by adding public IP - handle backward compatibility
    
    ---------
    
    Co-authored-by: Pearl Dsilva <pe...@gmail.com>
    
    * Netris - Support for L2 networks
    
    * Add support for L2 networks in Netris
    
    * remove the need to add connectivity to identify netris provider, remove changes to ui and create net offering
    
    * create vpc network and use it for l2 networks
    
    * dont fail if deletion on netris doesnt succeed
    
    * remove connectivity capability
    
    * update vpc name when l2 name is updated
    
    ---------
    
    Co-authored-by: Pearl Dsilva <pe...@gmail.com>
    
    * Netris - CKS support
    
    Co-authored-by: Pearl Dsilva <pe...@gmail.com>
    
    * Netris - Enable Global Routing Flag for NATTED Dual-Stack VPCs
    
    * Set globalRouting = true, when network offering is NATTED, dual-stack
    
    * update global routing based on routing mode and ip protocol type
    
    ---------
    
    Co-authored-by: Pearl Dsilva <pe...@gmail.com>
    Co-authored-by: nvazquez <ni...@gmail.com>
    
    * Netris - Fix allocate vnets with the correct account ID so that release succeeds on network deletion
    
    * Fix allocate vnets with the correct account ID so that release succeeds on network deletion
    
    * add fix for vpc tiers as well
    
    * move entire vnet cleanup logic to trash from shutdown, so that it works for all network types for netris
    
    * cleanup vnets when reservation id is null
    
    ---------
    
    Co-authored-by: Pearl Dsilva <pe...@gmail.com>
    
    * Netris - Add support to include VPN service for NATTED Netris offerings
    
    * Add support to include VPN service for NATTED Netris offerings
    
    * missing changes of global routing
    
    * Remove extra space
    
    ---------
    
    Co-authored-by: Pearl Dsilva <pe...@gmail.com>
    Co-authored-by: nvazquez <ni...@gmail.com>
    
    * Netris - Support Redundant Virtual Routers for Netris VPC networks
    
    * Add support for redundant routers for Netris VPC networks
    
    * address comment
    
    * default null network mode to natted for netris
    
    * preventing vpc and ipam creation if already exists
    
    ---------
    
    Co-authored-by: Pearl Dsilva <pe...@gmail.com>
    
    * Netris - Enable autoscaling groups for Netris network offerings
    
    * Enable autoscaling groups for Netris network offerings
    
    * Fix UI loading for services when network mode changes for external providers and show vm autoscaling always set to true / non-editable for netris when LB is selected i.e, natted mode
    
    * Revert removed comment
    
    ---------
    
    Co-authored-by: Pearl Dsilva <pe...@gmail.com>
    Co-authored-by: nvazquez <ni...@gmail.com>
    
    * merge conflict
    
    * systemvm: fix VPC VPN issue when network id is bigger than 1000 on Netris
    
    * show networks for vnf nic mappings
    
    * Add a check to prevent re-adding existing ACLs
    
    * Add a check to prevent re-adding existing ACLs
    
    * change message compare statement
    
    * Add check for vnet name length and cleanup vpc and vnet resources on failure of creation of allocation
    
    * add name length validation to update path
    
    ---------
    
    Co-authored-by: Pearl Dsilva <pe...@gmail.com>
    Co-authored-by: nvazquez <ni...@gmail.com>
    Co-authored-by: Wei Zhou <we...@apache.org>
---
 .../resource/NetrisResourceObjectUtils.java        | 12 ++++
 .../cloudstack/service/NetrisApiClientImpl.java    | 66 +++++++++++++++++++---
 2 files changed, 69 insertions(+), 9 deletions(-)

diff --git a/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/resource/NetrisResourceObjectUtils.java b/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/resource/NetrisResourceObjectUtils.java
index d67054398f6..5b7b9d8973d 100644
--- a/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/resource/NetrisResourceObjectUtils.java
+++ b/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/resource/NetrisResourceObjectUtils.java
@@ -16,6 +16,7 @@
 // under the License.
 package org.apache.cloudstack.resource;
 
+import com.cloud.exception.InvalidParameterValueException;
 import com.cloud.offering.NetworkOffering;
 import org.apache.cloudstack.agent.api.NetrisCommand;
 import org.apache.commons.lang3.ArrayUtils;
@@ -24,6 +25,8 @@ import java.util.Objects;
 
 public class NetrisResourceObjectUtils {
 
+    public static final int NETRIS_VNET_NAME_MAX_LENGTH = 50;
+
     public enum NetrisObjectType {
         VPC, IPAM_ALLOCATION, IPAM_SUBNET, VNET, SNAT, STATICNAT, DNAT, STATICROUTE, ACL, LB
     }
@@ -102,6 +105,15 @@ public class NetrisResourceObjectUtils {
         return stringBuilder.toString();
     }
 
+    public static void validateNetrisVnetNameLength(String netrisVnetName, String networkName) {
+        if (netrisVnetName.length() > NETRIS_VNET_NAME_MAX_LENGTH) {
+            throw new InvalidParameterValueException(
+                    String.format("The resulting Netris vNet name '%s' exceeds the maximum allowed length of %d characters (actual: %d). " +
+                            "Please use a shorter network name (current: '%s')",
+                            netrisVnetName, NETRIS_VNET_NAME_MAX_LENGTH, netrisVnetName.length(), networkName));
+        }
+    }
+
     public static NetworkOffering.NetworkMode getNetworkMode(NetworkOffering.NetworkMode mode) {
         return Objects.isNull(mode) ? NetworkOffering.NetworkMode.NATTED : mode;
     }
diff --git a/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/service/NetrisApiClientImpl.java b/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/service/NetrisApiClientImpl.java
index cb4ed59333b..bc6dd912be5 100644
--- a/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/service/NetrisApiClientImpl.java
+++ b/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/service/NetrisApiClientImpl.java
@@ -345,8 +345,19 @@ public class NetrisApiClientImpl implements NetrisApiClient {
 
         String netrisIpamAllocationName = NetrisResourceObjectUtils.retrieveNetrisResourceObjectName(cmd, NetrisResourceObjectUtils.NetrisObjectType.IPAM_ALLOCATION, cmd.getCidr());
         String vpcCidr = cmd.getCidr();
-        InlineResponse2004Data createdIpamAllocation = createIpamAllocationInternal(netrisIpamAllocationName, vpcCidr, createdVpc.getData());
-        return createdIpamAllocation != null;
+        VPCListing vpcListing = createdVpc.getData();
+        InlineResponse2004Data createdIpamAllocation = createIpamAllocationInternal(netrisIpamAllocationName, vpcCidr, vpcListing);
+        if (createdIpamAllocation == null) {
+            logger.warn("IPAM Allocation creation failed for VPC {}, rolling back VPC", netrisVpcName);
+            try {
+                deleteVpcInternal(vpcListing);
+            } catch (Exception e) {
+                logger.error("Failed to rollback VPC {} after IPAM Allocation failure - manual cleanup may be required: {}",
+                        netrisVpcName, e.getMessage());
+            }
+            return false;
+        }
+        return true;
     }
 
     @Override
@@ -1240,8 +1251,13 @@ public class NetrisApiClientImpl implements NetrisApiClient {
         }
         String netrisV6Cidr = cmd.getIpv6Cidr();
 
+        VPCListing associatedVpc = null;
+        String netrisSubnetName = null;
+        String netrisV6IpamAllocationName = null;
+        String netrisV6SubnetName = null;
+        boolean createdIpv6Allocation = false;
+
         try {
-            VPCListing associatedVpc;
             if (isL2) {
                 associatedVpc = getOrCreateL2Vpc(cmd);
                 if (associatedVpc == null) {
@@ -1264,19 +1280,22 @@ public class NetrisApiClientImpl implements NetrisApiClient {
                 vNetName = String.format("N%s-%s", networkId, networkName);
             }
             String netrisVnetName = NetrisResourceObjectUtils.retrieveNetrisResourceObjectName(cmd, NetrisResourceObjectUtils.NetrisObjectType.VNET, vNetName);
+            NetrisResourceObjectUtils.validateNetrisVnetNameLength(netrisVnetName, networkName);
 
             if (!isL2) {
-                String netrisSubnetName = NetrisResourceObjectUtils.retrieveNetrisResourceObjectName(cmd, NetrisResourceObjectUtils.NetrisObjectType.IPAM_SUBNET, String.valueOf(cmd.getVpcId()), vnetCidr);
+                netrisSubnetName = NetrisResourceObjectUtils.retrieveNetrisResourceObjectName(cmd, NetrisResourceObjectUtils.NetrisObjectType.IPAM_SUBNET, String.valueOf(cmd.getVpcId()), vnetCidr);
                 createIpamSubnetInternal(netrisSubnetName, vnetCidr, SubnetBody.PurposeEnum.COMMON, associatedVpc, ipv4GlobalRouting);
                 if (Objects.nonNull(netrisV6Cidr)) {
-                    String netrisV6IpamAllocationName = NetrisResourceObjectUtils.retrieveNetrisResourceObjectName(cmd, NetrisResourceObjectUtils.NetrisObjectType.IPAM_ALLOCATION, netrisV6Cidr);
-                    String netrisV6SubnetName = NetrisResourceObjectUtils.retrieveNetrisResourceObjectName(cmd, NetrisResourceObjectUtils.NetrisObjectType.IPAM_SUBNET, String.valueOf(cmd.getVpcId()), netrisV6Cidr);
+                    netrisV6IpamAllocationName = NetrisResourceObjectUtils.retrieveNetrisResourceObjectName(cmd, NetrisResourceObjectUtils.NetrisObjectType.IPAM_ALLOCATION, netrisV6Cidr);
+                    netrisV6SubnetName = NetrisResourceObjectUtils.retrieveNetrisResourceObjectName(cmd, NetrisResourceObjectUtils.NetrisObjectType.IPAM_SUBNET, String.valueOf(cmd.getVpcId()), netrisV6Cidr);
                     BigDecimal ipamAllocationId = getIpamAllocationIdByPrefixAndVpc(netrisV6Cidr, associatedVpc);
                     if (ipamAllocationId == null) {
-                        InlineResponse2004Data createdIpamAllocation = createIpamAllocationInternal(netrisV6IpamAllocationName, netrisV6Cidr, associatedVpc);
-                        if (Objects.isNull(createdIpamAllocation)) {
+                        InlineResponse2004Data createdIpamAllocationData = createIpamAllocationInternal(netrisV6IpamAllocationName, netrisV6Cidr, associatedVpc);
+                        if (Objects.isNull(createdIpamAllocationData)) {
+                            rollbackVnetResources(associatedVpc, netrisSubnetName, null, null, networkName);
                             throw new CloudRuntimeException(String.format("Failed to create Netris IPAM Allocation %s for VPC %s", netrisV6IpamAllocationName, associatedVpc.getName()));
                         }
+                        createdIpv6Allocation = true;
                     }
                     createIpamSubnetInternal(netrisV6SubnetName, netrisV6Cidr, SubnetBody.PurposeEnum.COMMON, associatedVpc, ipv6GlobalRouting);
                 }
@@ -1287,9 +1306,17 @@ public class NetrisApiClientImpl implements NetrisApiClient {
             if (vnetResponse == null || !vnetResponse.isIsSuccess()) {
                 String reason = vnetResponse == null ? "Empty response" : "Operation failed on Netris";
                 logger.debug("The Netris vNet creation {} failed: {}", vNetName, reason);
+                if (!isL2) {
+                    rollbackVnetResources(associatedVpc, netrisSubnetName, netrisV6SubnetName,
+                            createdIpv6Allocation ? netrisV6IpamAllocationName : null, networkName);
+                }
                 return false;
             }
-        } catch (ApiException e) {
+        } catch (Exception e) {
+            if (!isL2 && associatedVpc != null) {
+                rollbackVnetResources(associatedVpc, netrisSubnetName, netrisV6SubnetName,
+                        createdIpv6Allocation ? netrisV6IpamAllocationName : null, networkName);
+            }
             throw new CloudRuntimeException(String.format("Failed to create Netris vNet %s", networkName), e);
         }
         return true;
@@ -1324,6 +1351,7 @@ public class NetrisApiClientImpl implements NetrisApiClient {
         String netrisVnetName = NetrisResourceObjectUtils.retrieveNetrisResourceObjectName(cmd, NetrisResourceObjectUtils.NetrisObjectType.VNET, vNetName) ;
         String prevNetrisVnetName = NetrisResourceObjectUtils.retrieveNetrisResourceObjectName(cmd, NetrisResourceObjectUtils.NetrisObjectType.VNET, prevVnetName) ;
 
+        NetrisResourceObjectUtils.validateNetrisVnetNameLength(netrisVnetName, networkName);
         VnetResAddBody response = updateVnetInternal(associatedVpc, netrisVnetName, prevNetrisVnetName);
         if (response == null || !response.isIsSuccess()) {
             String reason = response == null ? "Empty response" : "Operation failed on Netris";
@@ -1958,6 +1986,26 @@ public class NetrisApiClientImpl implements NetrisApiClient {
         }
     }
 
+    private void rollbackVnetResources(VPCListing associatedVpc, String ipv4SubnetName, String ipv6SubnetName,
+                                       String ipv6AllocationName, String networkName) {
+        logger.warn("Rolling back Netris resources created for network {}", networkName);
+        FilterByVpc vpcFilter = new FilterByVpc();
+        vpcFilter.add(associatedVpc.getId());
+        try {
+            if (ipv6SubnetName != null) {
+                deleteSubnetInternal(vpcFilter, null, ipv6SubnetName);
+            }
+            if (ipv6AllocationName != null) {
+                deleteVpcIpamAllocationInternal(associatedVpc, ipv6AllocationName);
+            }
+            if (ipv4SubnetName != null) {
+                deleteSubnetInternal(vpcFilter, null, ipv4SubnetName);
+            }
+        } catch (Exception e) {
+            logger.error("Failed to rollback Netris resources for network {} - manual cleanup may be required: {}", networkName, e.getMessage());
+        }
+    }
+
     private SubnetBody getIpamSubnetBody(VPCListing vpc, SubnetBody.PurposeEnum purpose, String subnetName, String subnetPrefix, Boolean isGlobalRouting) {
         SubnetBody subnetBody = new SubnetBody();
         subnetBody.setName(subnetName);


(cloudstack) 08/08: Allocate IP from Netris (Provider) range when IP is not specified during associate

Posted by pe...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

Pearl1594 pushed a commit to branch netris-improvements
in repository https://gitbox.apache.org/repos/asf/cloudstack.git

commit 0c0f22c9ad35a576e58c362623b6048a70868e7e
Author: Pearl Dsilva <pe...@gmail.com>
AuthorDate: Fri Jul 10 09:27:10 2026 -0400

    Allocate IP from Netris (Provider) range when IP is not specified during associate
    
    * Allocate IP from Netris (Provider) range when IP is not specified during associate
    
    * update javadoc and method name
    
    ---------
    
    Co-authored-by: Pearl Dsilva <pe...@gmail.com>
---
 .../com/cloud/network/IpAddressManagerImpl.java    | 35 ++++++++++++++++++++--
 1 file changed, 32 insertions(+), 3 deletions(-)

diff --git a/server/src/main/java/com/cloud/network/IpAddressManagerImpl.java b/server/src/main/java/com/cloud/network/IpAddressManagerImpl.java
index 6a4f3745f1b..2d5102d8848 100644
--- a/server/src/main/java/com/cloud/network/IpAddressManagerImpl.java
+++ b/server/src/main/java/com/cloud/network/IpAddressManagerImpl.java
@@ -1417,6 +1417,31 @@ public class IpAddressManagerImpl extends ManagerBase implements IpAddressManage
         }
     }
 
+    /**
+     * When the zone is linked to external provider Netris and no explicit IP was requested:
+     * restrict the candidate pool to the VLANs reserved for Netris, so that generic acquisitions
+     * (e.g. associateIpAddress called by the CloudStack cloud-controller-manager on behalf of a
+     * Kubernetes Service of type LoadBalancer) don't fall back to the VR's public range.
+     * Returns null when the zone has no Netris provider, meaning no restriction is applied.
+     *
+     * returns VLAN IDS tagged with forNetris = true
+     */
+    private List<Long> getNetrisVlanDbIds(DataCenter zone) {
+        long zoneId = zone.getId();
+        NetrisProviderVO netrisProvider = netrisProviderDao.findByZoneId(zoneId);
+        if (netrisProvider == null) {
+            return null;
+        }
+        List<Long> vlanIds = _vlanDao.listVlansForExternalNetworkProvider(zoneId, ApiConstants.NETRIS_DETAIL_KEY).stream()
+                .map(VlanVO::getId).collect(Collectors.toList());
+        if (vlanIds.isEmpty()) {
+            throw new CloudRuntimeException(String.format(
+                    "Cannot allocate public IP in zone %s: no VLANs tagged '%s=true' found for the external provider",
+                    zone.getName(), ApiConstants.NETRIS_DETAIL_KEY));
+        }
+        return vlanIds;
+    }
+
     @DB
     @Override
     public IpAddress allocateIp(final Account ipOwner, final boolean isSystem, Account caller, User callerUser, final DataCenter zone, final Boolean displayIp, final String ipaddress)
@@ -1425,8 +1450,6 @@ public class IpAddressManagerImpl extends ManagerBase implements IpAddressManage
         final VlanType vlanType = VlanType.VirtualNetwork;
         final boolean assign = false;
 
-        checkPublicIpOnExternalProviderZone(zone, ipaddress);
-
         if (Grouping.AllocationState.Disabled == zone.getAllocationState() && !_accountMgr.isRootAdmin(caller.getId())) {
             // zone is of type DataCenter. See DataCenterVO.java.
             PermissionDeniedException ex = new PermissionDeniedException(generateErrorMessageForOperationOnDisabledZone("allocate IP addresses", zone));
@@ -1434,6 +1457,12 @@ public class IpAddressManagerImpl extends ManagerBase implements IpAddressManage
             throw ex;
         }
 
+        checkPublicIpOnExternalProviderZone(zone, ipaddress);
+
+        // Only steer the range when no explicit IP was requested: an explicit ipaddress is already
+        // validated against the provider's pool above by checkPublicIpOnExternalProviderZone.
+        final List<Long> vlanDbIds = ipaddress == null ? getNetrisVlanDbIds(zone) : null;
+
         PublicIp ip = null;
 
         Account accountToLock = null;
@@ -1461,7 +1490,7 @@ public class IpAddressManagerImpl extends ManagerBase implements IpAddressManage
             ip = Transaction.execute(new TransactionCallbackWithException<PublicIp, InsufficientAddressCapacityException>() {
                 @Override
                 public PublicIp doInTransaction(TransactionStatus status) throws InsufficientAddressCapacityException {
-                    PublicIp ip = fetchNewPublicIp(zone.getId(), null, null, ipOwner, vlanType, null, false, assign, ipaddress, null, isSystem, null, displayIp, false);
+                    PublicIp ip = fetchNewPublicIp(zone.getId(), null, vlanDbIds, ipOwner, vlanType, null, false, assign, ipaddress, null, isSystem, null, displayIp, false);
 
                     if (ip == null) {
                         InsufficientAddressCapacityException ex = new InsufficientAddressCapacityException("Unable to find available public IP addresses", DataCenter.class, zone


(cloudstack) 04/08: Add support to the VPC response to display if redundant routers are supported

Posted by pe...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

Pearl1594 pushed a commit to branch netris-improvements
in repository https://gitbox.apache.org/repos/asf/cloudstack.git

commit 34a15b5bc2d8f1b40d4f9c9018f40f405ef9389b
Author: Pearl Dsilva <pe...@gmail.com>
AuthorDate: Mon Jul 6 09:17:27 2026 -0400

    Add support to the VPC response to display if redundant routers are supported
    
    Co-authored-by: Pearl Dsilva <pe...@gmail.com>
---
 .../org/apache/cloudstack/api/response/VpcOfferingResponse.java   | 8 ++++++++
 .../main/java/com/cloud/api/query/dao/VpcOfferingJoinDaoImpl.java | 1 +
 ui/src/config/section/offering.js                                 | 2 +-
 3 files changed, 10 insertions(+), 1 deletion(-)

diff --git a/api/src/main/java/org/apache/cloudstack/api/response/VpcOfferingResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/VpcOfferingResponse.java
index a0516e660e4..6c26a45b482 100644
--- a/api/src/main/java/org/apache/cloudstack/api/response/VpcOfferingResponse.java
+++ b/api/src/main/java/org/apache/cloudstack/api/response/VpcOfferingResponse.java
@@ -62,6 +62,10 @@ public class VpcOfferingResponse extends BaseResponse {
     @Param(description = "Indicates if the VPC offering supports distributed router for one-hop forwarding", since = "4.4")
     private Boolean supportsDistributedRouter;
 
+    @SerializedName(ApiConstants.REDUNDANT_ROUTER)
+    @Param(description = "Indicates if the VPC offering supports redundant routers")
+    private Boolean redundantRouter;
+
     @SerializedName((ApiConstants.SUPPORTS_REGION_LEVEL_VPC))
     @Param(description = "Indicated if the offering can support region level VPC", since = "4.4")
     private Boolean supportsRegionLevelVpc;
@@ -201,4 +205,8 @@ public class VpcOfferingResponse extends BaseResponse {
     public void setRoutingMode(String routingMode) {
         this.routingMode = routingMode;
     }
+
+    public void setRedundantRouter(Boolean redundantRouter) {
+        this.redundantRouter = redundantRouter;
+    }
 }
diff --git a/server/src/main/java/com/cloud/api/query/dao/VpcOfferingJoinDaoImpl.java b/server/src/main/java/com/cloud/api/query/dao/VpcOfferingJoinDaoImpl.java
index 7ea4b7d5834..8f6f6199085 100644
--- a/server/src/main/java/com/cloud/api/query/dao/VpcOfferingJoinDaoImpl.java
+++ b/server/src/main/java/com/cloud/api/query/dao/VpcOfferingJoinDaoImpl.java
@@ -71,6 +71,7 @@ public class VpcOfferingJoinDaoImpl extends GenericDaoBase<VpcOfferingJoinVO, Lo
         offeringResponse.setSupportsDistributedRouter(offering.isSupportsDistributedRouter());
         offeringResponse.setSupportsRegionLevelVpc(offering.isOffersRegionLevelVPC());
         offeringResponse.setCreated(offering.getCreated());
+        offeringResponse.setRedundantRouter(offering.isRedundantRouter());
         if (offering.getRoutingMode() != null) {
             offeringResponse.setRoutingMode(offering.getRoutingMode().toString());
         }
diff --git a/ui/src/config/section/offering.js b/ui/src/config/section/offering.js
index 4a32619b8c2..1433533beab 100644
--- a/ui/src/config/section/offering.js
+++ b/ui/src/config/section/offering.js
@@ -508,7 +508,7 @@ export default {
       searchFilters: ['name', 'zoneid', 'domainid'],
       resourceType: 'VpcOffering',
       columns: ['name', 'state', 'displaytext', 'domain', 'zone', 'order'],
-      details: ['name', 'id', 'displaytext', 'internetprotocol', 'distributedvpcrouter', 'tags', 'routingmode', 'specifyasnumber', 'service', 'fornsx', 'networkmode', 'domain', 'zone', 'created'],
+      details: ['name', 'id', 'displaytext', 'internetprotocol', 'distributedvpcrouter', 'redundantrouter', 'tags', 'routingmode', 'specifyasnumber', 'service', 'fornsx', 'networkmode', 'domain', 'zone', 'created'],
       related: [{
         name: 'vpc',
         title: 'label.vpc',


(cloudstack) 01/08: Netris CloudStack Integration Enhancements

Posted by pe...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

Pearl1594 pushed a commit to branch netris-improvements
in repository https://gitbox.apache.org/repos/asf/cloudstack.git

commit 4b817d8d6e823fd9617ebb2e2d8344df30436f4f
Author: Pearl Dsilva <pe...@gmail.com>
AuthorDate: Wed Jun 17 23:31:52 2026 -0400

    Netris CloudStack Integration Enhancements
    
    * Netris - Support NAT, PF for Secondary IP
    
    * Allow PF rules to be correctly created on secondary IP of a VM on netris
    
    * unique static nat names on netris by adding public IP - handle backward compatibility
    
    ---------
    
    Co-authored-by: Pearl Dsilva <pe...@gmail.com>
    
    * Netris - Support for L2 networks
    
    * Add support for L2 networks in Netris
    
    * remove the need to add connectivity to identify netris provider, remove changes to ui and create net offering
    
    * create vpc network and use it for l2 networks
    
    * dont fail if deletion on netris doesnt succeed
    
    * remove connectivity capability
    
    * update vpc name when l2 name is updated
    
    ---------
    
    Co-authored-by: Pearl Dsilva <pe...@gmail.com>
    
    * Netris - CKS support
    
    Co-authored-by: Pearl Dsilva <pe...@gmail.com>
    
    * Netris - Enable Global Routing Flag for NATTED Dual-Stack VPCs
    
    * Set globalRouting = true, when network offering is NATTED, dual-stack
    
    * update global routing based on routing mode and ip protocol type
    
    ---------
    
    Co-authored-by: Pearl Dsilva <pe...@gmail.com>
    Co-authored-by: nvazquez <ni...@gmail.com>
    
    * Netris - Fix allocate vnets with the correct account ID so that release succeeds on network deletion
    
    * Fix allocate vnets with the correct account ID so that release succeeds on network deletion
    
    * add fix for vpc tiers as well
    
    * move entire vnet cleanup logic to trash from shutdown, so that it works for all network types for netris
    
    * cleanup vnets when reservation id is null
    
    ---------
    
    Co-authored-by: Pearl Dsilva <pe...@gmail.com>
    
    * Netris - Add support to include VPN service for NATTED Netris offerings
    
    * Add support to include VPN service for NATTED Netris offerings
    
    * missing changes of global routing
    
    * Remove extra space
    
    ---------
    
    Co-authored-by: Pearl Dsilva <pe...@gmail.com>
    Co-authored-by: nvazquez <ni...@gmail.com>
    
    * Netris - Support Redundant Virtual Routers for Netris VPC networks
    
    * Add support for redundant routers for Netris VPC networks
    
    * address comment
    
    * default null network mode to natted for netris
    
    * preventing vpc and ipam creation if already exists
    
    ---------
    
    Co-authored-by: Pearl Dsilva <pe...@gmail.com>
    
    * Netris - Enable autoscaling groups for Netris network offerings
    
    * Enable autoscaling groups for Netris network offerings
    
    * Fix UI loading for services when network mode changes for external providers and show vm autoscaling always set to true / non-editable for netris when LB is selected i.e, natted mode
    
    * Revert removed comment
    
    ---------
    
    Co-authored-by: Pearl Dsilva <pe...@gmail.com>
    Co-authored-by: nvazquez <ni...@gmail.com>
    
    * merge conflict
    
    * systemvm: fix VPC VPN issue when network id is bigger than 1000 on Netris
    
    * show networks for vnf nic mappings
    
    ---------
    
    Co-authored-by: Pearl Dsilva <pe...@gmail.com>
    Co-authored-by: nvazquez <ni...@gmail.com>
    Co-authored-by: Wei Zhou <we...@apache.org>
---
 .../com/cloud/network/netris/NetrisService.java    |  30 +-
 .../org/apache/cloudstack/api/ApiConstants.java    |   1 +
 .../admin/network/CreateNetworkOfferingCmd.java    |   8 +
 .../command/admin/vpc/CreateVPCOfferingCmd.java    |  28 +-
 .../api/command/utils/OfferingUtils.java           |   8 +-
 .../engine/orchestration/NetworkOrchestrator.java  |   5 +-
 .../cluster/KubernetesClusterManagerImpl.java      |  49 +++-
 .../KubernetesClusterActionWorker.java             |  98 ++++++-
 .../KubernetesClusterDestroyWorker.java            |  43 ++-
 ...ernetesClusterResourceModifierActionWorker.java |  23 ++
 .../KubernetesClusterScaleWorker.java              |  26 +-
 .../KubernetesClusterStartWorker.java              |  61 +++-
 .../agent/api/CreateNetrisVnetCommand.java         |  22 +-
 .../apache/cloudstack/agent/api/NetrisCommand.java |   9 +
 .../resource/NetrisResourceObjectUtils.java        |   7 +-
 .../cloudstack/service/NetrisApiClientImpl.java    | 315 ++++++++++++++++-----
 .../apache/cloudstack/service/NetrisElement.java   |  16 +-
 .../cloudstack/service/NetrisGuestNetworkGuru.java | 146 ++++++++--
 .../service/NetrisPublicNetworkGuru.java           |   9 +-
 .../cloudstack/service/NetrisServiceImpl.java      |  26 +-
 .../agent/api/CreateNetrisVnetCommandTest.java     |  11 +-
 .../resource/NetrisResourceObjectUtilsTest.java    |   6 +-
 .../cloudstack/service/NetrisElementTest.java      |   2 +-
 .../service/NetrisGuestNetworkGuruTest.java        |  42 ++-
 .../com/cloud/network/guru/GuestNetworkGuru.java   |  16 +-
 .../java/com/cloud/network/vpc/VpcManagerImpl.java |   7 +-
 .../cloudstack/service/NetrisServiceMockTest.java  |   8 +-
 ui/src/views/offering/AddNetworkOffering.vue       | 187 ++++++------
 ui/src/views/offering/AddVpcOffering.vue           |  26 +-
 29 files changed, 933 insertions(+), 302 deletions(-)

diff --git a/api/src/main/java/com/cloud/network/netris/NetrisService.java b/api/src/main/java/com/cloud/network/netris/NetrisService.java
index 110e9f07105..1e7c7d66399 100644
--- a/api/src/main/java/com/cloud/network/netris/NetrisService.java
+++ b/api/src/main/java/com/cloud/network/netris/NetrisService.java
@@ -21,6 +21,8 @@ import com.cloud.network.Network;
 import com.cloud.network.SDNProviderNetworkRule;
 import com.cloud.network.vpc.StaticRoute;
 import com.cloud.network.vpc.Vpc;
+import com.cloud.offering.NetworkOffering;
+import com.cloud.utils.net.NetUtils;
 
 import java.util.List;
 
@@ -81,18 +83,20 @@ public interface NetrisService {
     /**
      * Creates a virtual network (vNet) resource.
      *
-     * @param zoneId           the ID of the zone
-     * @param accountId        the ID of the account
-     * @param domainId         the ID of the domain
-     * @param vpcName          the name of the VPC
-     * @param vpcId            the ID of the VPC
-     * @param networkName      the name of the network
-     * @param networkId        the ID of the network
-     * @param cidr             the CIDR of the network
-     * @param globalRouting    true if global routing is enabled
+     * @param zoneId            the ID of the zone
+     * @param accountId         the ID of the account
+     * @param domainId          the ID of the domain
+     * @param vpcName           the name of the VPC
+     * @param vpcId             the ID of the VPC
+     * @param networkName       the name of the network
+     * @param networkId         the ID of the network
+     * @param cidr              the CIDR of the network
+     * @param networkMode       the network mode
+     * @param internetProtocol  the internet protocol
+     * @param isL2              true if it is an L2 network
      * @return true if the operation is successful, false otherwise
      */
-    boolean createVnetResource(Long zoneId, long accountId, long domainId, String vpcName, Long vpcId, String networkName, Long networkId, String cidr, Boolean globalRouting);
+    boolean createVnetResource(Long zoneId, long accountId, long domainId, String vpcName, Long vpcId, String networkName, Long networkId, String cidr, NetworkOffering.NetworkMode networkMode, NetUtils.InternetProtocol internetProtocol, boolean isL2);
 
     /**
      * Updates an existing vNet resource.
@@ -105,9 +109,10 @@ public interface NetrisService {
      * @param networkName     the new name of the network
      * @param networkId       the ID of the network
      * @param prevNetworkName the previous name of the network
+     * @param isL2            true if it is an L2 network
      * @return true if the operation is successful, false otherwise
      */
-    boolean updateVnetResource(Long zoneId, long accountId, long domainId, String vpcName, Long vpcId, String networkName, Long networkId, String prevNetworkName);
+    boolean updateVnetResource(Long zoneId, long accountId, long domainId, String vpcName, Long vpcId, String networkName, Long networkId, String prevNetworkName, boolean isL2);
 
     /**
      * Deletes an existing vNet resource.
@@ -120,9 +125,10 @@ public interface NetrisService {
      * @param networkName  the name of the network
      * @param networkId    the ID of the network
      * @param cidr         the CIDR of the network
+     * @param isL2         true if it is an L2 network
      * @return true if the operation is successful, false otherwise
      */
-    boolean deleteVnetResource(long zoneId, long accountId, long domainId, String vpcName, Long vpcId, String networkName, Long networkId, String cidr);
+    boolean deleteVnetResource(long zoneId, long accountId, long domainId, String vpcName, Long vpcId, String networkName, Long networkId, String cidr, boolean isL2);
 
     /**
      * Creates a source NAT rule for a VPC or network.
diff --git a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java
index a8ff00c40ff..05d26278101 100644
--- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java
+++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java
@@ -1288,6 +1288,7 @@ public class ApiConstants {
     public static final String HAS_RULES = "hasrules";
     public static final String NSX_DETAIL_KEY = "forNsx";
     public static final String NETRIS_DETAIL_KEY = "forNetris";
+    public static final String NETRIS_NAT_PUBLIC_IP_ID = "netrisnatpublicipid";
     public static final String NETRIS_TAG = "netristag";
     public static final String NETRIS_VXLAN_ID = "netrisvxlanid";
     public static final String NETRIS_URL = "netrisurl";
diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/CreateNetworkOfferingCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/CreateNetworkOfferingCmd.java
index a0559f57dab..da71b6596c0 100644
--- a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/CreateNetworkOfferingCmd.java
+++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/CreateNetworkOfferingCmd.java
@@ -60,6 +60,7 @@ import static com.cloud.network.Network.Service.PortForwarding;
 import static com.cloud.network.Network.Service.NetworkACL;
 import static com.cloud.network.Network.Service.UserData;
 import static com.cloud.network.Network.Service.Firewall;
+import static com.cloud.network.Network.Service.Vpn;
 
 import static org.apache.cloudstack.api.command.utils.OfferingUtils.isNetrisNatted;
 import static org.apache.cloudstack.api.command.utils.OfferingUtils.isNetrisRouted;
@@ -309,6 +310,10 @@ public class CreateNetworkOfferingCmd extends BaseCmd {
             } else {
                 services.add(Firewall.getName());
             }
+
+            if (isNetrisNatted(getProvider(), getNetworkMode())) {
+                services.add(Vpn.getName());
+            }
             return services;
         }
     }
@@ -418,6 +423,9 @@ public class CreateNetworkOfferingCmd extends BaseCmd {
                 serviceProviderMap.remove(Lb.getName());
             }
         }
+        if (isNetrisNatted(getProvider(), getNetworkMode())) {
+            serviceProviderMap.put(Vpn.getName(), List.of(routerProvider));
+        }
     }
 
     public Map<Capability, String> getServiceCapabilities(Service service) {
diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/vpc/CreateVPCOfferingCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/vpc/CreateVPCOfferingCmd.java
index 608794842ed..0897a2b1b42 100644
--- a/api/src/main/java/org/apache/cloudstack/api/command/admin/vpc/CreateVPCOfferingCmd.java
+++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/vpc/CreateVPCOfferingCmd.java
@@ -24,6 +24,7 @@ import java.util.Iterator;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
@@ -61,7 +62,9 @@ import static com.cloud.network.Network.Service.PortForwarding;
 import static com.cloud.network.Network.Service.NetworkACL;
 import static com.cloud.network.Network.Service.UserData;
 import static com.cloud.network.Network.Service.Gateway;
+import static com.cloud.network.Network.Service.Vpn;
 
+import static org.apache.cloudstack.api.command.utils.OfferingUtils.isNetris;
 import static org.apache.cloudstack.api.command.utils.OfferingUtils.isNetrisNatted;
 import static org.apache.cloudstack.api.command.utils.OfferingUtils.isNetrisRouted;
 import static org.apache.cloudstack.api.command.utils.OfferingUtils.isNsxWithoutLb;
@@ -189,7 +192,7 @@ public class CreateVPCOfferingCmd extends BaseAsyncCreateCmd {
                     NetworkACL.getName(),
                     UserData.getName()
                     ));
-            if (NetworkOffering.NetworkMode.NATTED.name().equalsIgnoreCase(getNetworkMode())) {
+            if (isNetrisNatted(getProvider(), getNetworkMode())) {
                 supportedServices.addAll(Arrays.asList(
                         StaticNat.getName(),
                         SourceNat.getName(),
@@ -198,9 +201,13 @@ public class CreateVPCOfferingCmd extends BaseAsyncCreateCmd {
             if (NetworkOffering.NetworkMode.ROUTED.name().equalsIgnoreCase(getNetworkMode())) {
                 supportedServices.add(Gateway.getName());
             }
+
             if (getNsxSupportsLbService() || isNetrisNatted(getProvider(), getNetworkMode())) {
                 supportedServices.add(Lb.getName());
             }
+            if (isNetrisNatted(getProvider(), getNetworkMode())) {
+                supportedServices.add(Vpn.getName());
+            }
         }
         return supportedServices;
     }
@@ -210,7 +217,7 @@ public class CreateVPCOfferingCmd extends BaseAsyncCreateCmd {
     }
 
     public String getNetworkMode() {
-        return networkMode;
+        return isNetris(getProvider()) && Objects.isNull(networkMode) ? NetworkOffering.NetworkMode.NATTED.name() : networkMode;
     }
 
     public boolean getNsxSupportsLbService() {
@@ -248,7 +255,7 @@ public class CreateVPCOfferingCmd extends BaseAsyncCreateCmd {
 
     private void getServiceProviderMapForExternalProvider(Map<String, List<String>> serviceProviderMap, String provider) {
         List<String> unsupportedServices = new ArrayList<>(List.of("Vpn", "BaremetalPxeService", "SecurityGroup", "Connectivity", "Firewall"));
-        if (NetworkOffering.NetworkMode.NATTED.name().equalsIgnoreCase(getNetworkMode())) {
+        if (isNetrisNatted(getProvider(), getNetworkMode())) {
             unsupportedServices.add("Gateway");
         }
         List<String> routerSupported = List.of("Dhcp", "Dns", "UserData");
@@ -258,7 +265,7 @@ public class CreateVPCOfferingCmd extends BaseAsyncCreateCmd {
                 continue;
             if (routerSupported.contains(service))
                 serviceProviderMap.put(service, List.of(VirtualRouterProvider.Type.VPCVirtualRouter.name()));
-            else if (NetworkOffering.NetworkMode.NATTED.name().equalsIgnoreCase(getNetworkMode()) ||
+            else if (isNetrisNatted(getProvider(), getNetworkMode()) ||
                     Stream.of(NetworkACL.getName(), Gateway.getName()).anyMatch(s -> s.equalsIgnoreCase(service))) {
                 serviceProviderMap.put(service, List.of(provider));
             }
@@ -268,7 +275,20 @@ public class CreateVPCOfferingCmd extends BaseAsyncCreateCmd {
         }
     }
 
+    @SuppressWarnings("unchecked")
     public Map<String, List<String>> getServiceCapabilityList() {
+        // For Netris NATTED, Gateway service is excluded so the redundant-router capability
+        // must reference SourceNat (not Gateway).
+        if (serviceCapabilityList != null
+                && isNetrisNatted(getProvider(), getNetworkMode())) {
+            for (Object capEntry : serviceCapabilityList.values()) {
+                Map<String, String> cap = (Map<String, String>) capEntry;
+                if (Network.Capability.RedundantRouter.getName().equalsIgnoreCase(cap.get("capabilitytype"))
+                        && Gateway.getName().equalsIgnoreCase(cap.get("service"))) {
+                    cap.put("service", SourceNat.getName());
+                }
+            }
+        }
         return serviceCapabilityList;
     }
 
diff --git a/api/src/main/java/org/apache/cloudstack/api/command/utils/OfferingUtils.java b/api/src/main/java/org/apache/cloudstack/api/command/utils/OfferingUtils.java
index 433a37c07cd..3885c30f0af 100644
--- a/api/src/main/java/org/apache/cloudstack/api/command/utils/OfferingUtils.java
+++ b/api/src/main/java/org/apache/cloudstack/api/command/utils/OfferingUtils.java
@@ -23,8 +23,12 @@ public class OfferingUtils {
     private OfferingUtils() {
     }
 
+    public static boolean isNetris(String provider) {
+        return "Netris".equalsIgnoreCase(provider);
+    }
+
     public static boolean isNetrisNatted(String provider, String networkMode) {
-        return "Netris".equalsIgnoreCase(provider) &&
+        return isNetris(provider) &&
                NetworkOffering.NetworkMode.NATTED.name().equalsIgnoreCase(networkMode);
     }
 
@@ -33,6 +37,6 @@ public class OfferingUtils {
     }
 
     public static boolean isNetrisRouted(String provider, String networkMode) {
-        return "Netris".equalsIgnoreCase(provider) && NetworkOffering.NetworkMode.ROUTED.name().equalsIgnoreCase(networkMode);
+        return isNetris(provider) && NetworkOffering.NetworkMode.ROUTED.name().equalsIgnoreCase(networkMode);
     }
 }
diff --git a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java
index 55079173dc9..62e084ad2bf 100644
--- a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java
+++ b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java
@@ -3173,9 +3173,10 @@ public class NetworkOrchestrator extends ManagerBase implements NetworkOrchestra
     }
 
     /**
-     * Checks for L2 network offering services. Only 2 cases allowed:
+     * Checks for L2 network offering services. Only 3 cases allowed:
      * - No services
-     * - User Data service only, provided by ConfigDrive
+     * - UserData service only, provided by ConfigDrive
+     * - Connectivity service only, provided by Netris
      *
      * @param ntwkOff network offering
      */
diff --git a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesClusterManagerImpl.java b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesClusterManagerImpl.java
index d19470f8bab..9b9ed32e7ea 100644
--- a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesClusterManagerImpl.java
+++ b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesClusterManagerImpl.java
@@ -300,6 +300,8 @@ public class KubernetesClusterManagerImpl extends ManagerBase implements Kuberne
     private static final String DEFAULT_NSX_VPC_TIER_NETWORK_OFFERING_FOR_KUBERNETES_SERVICE_NAME = "DefaultNSXVPCNetworkOfferingforKubernetesService";
     private static final String DEFAULT_NSX_NETWORK_OFFERING_FOR_KUBERNETES_SERVICE_DISPLAY_TEXT = "Network Offering for NSX CloudStack Kubernetes Service";
     private static final String DEFAULT_NSX_VPC_NETWORK_OFFERING_FOR_KUBERNETES_SERVICE_DISPLAY_TEXT = "Network Offering for NSX CloudStack Kubernetes service on VPC";
+    private static final String DEFAULT_NETRIS_VPC_TIER_NETWORK_OFFERING_FOR_KUBERNETES_SERVICE_NAME = "DefaultNetrisVPCNetworkOfferingforKubernetesService";
+    private static final String DEFAULT_NETRIS_VPC_NETWORK_OFFERING_FOR_KUBERNETES_SERVICE_DISPLAY_TEXT = "Network Offering for Netris CloudStack Kubernetes Service on VPC";
 
     protected StateMachine2<KubernetesCluster.State, KubernetesCluster.Event, KubernetesCluster> _stateMachine = KubernetesCluster.State.getStateMachine();
 
@@ -598,6 +600,21 @@ public class KubernetesClusterManagerImpl extends ManagerBase implements Kuberne
         validateIsolatedNetworkIpRules(sourceNatIp.getId(), FirewallRule.Purpose.PortForwarding, network, clusterTotalNodeCount);
     }
 
+    public boolean isNetrisNetwork(Network network) {
+        return networkModel.isProviderSupportServiceInNetwork(
+                network.getId(), Service.Lb, Network.Provider.Netris);
+    }
+
+    protected void validateNetrisNetwork(Network network) {
+        if (network.getVpcId() == null) {
+            throw new InvalidParameterValueException(String.format(
+                    "Netris Kubernetes provider supports only VPC networks / tiers. " +
+                            "Network '%s' (id: %s) is not part of a VPC. " +
+                            "Please create the cluster within a VPC tier.",
+                    network.getName(), network.getUuid()));
+        }
+    }
+
     protected void validateVpcTier(Network network) {
         if (Network.State.Allocated.equals(network.getState())) { // Allocated networks won't have IP and rules
             return;
@@ -1146,6 +1163,9 @@ public class KubernetesClusterManagerImpl extends ManagerBase implements Kuberne
         Network network = null;
         if (networkId != null) {
             network = networkDao.findById(networkId);
+            if (isNetrisNetwork(network)) {
+                validateNetrisNetwork(network);
+            }
             if (Network.GuestType.Isolated.equals(network.getGuestType())) {
                 if (kubernetesClusterDao.listByNetworkId(network.getId()).isEmpty()) {
                     validateNetwork(network, controlNodesCount + nodesCount);
@@ -2689,13 +2709,16 @@ public class KubernetesClusterManagerImpl extends ManagerBase implements Kuberne
     @Override
     public boolean start() {
         createNetworkOfferingForKubernetes(DEFAULT_NETWORK_OFFERING_FOR_KUBERNETES_SERVICE_NAME,
-                DEFAULT_NETWORK_OFFERING_FOR_KUBERNETES_SERVICE_DISPLAY_TEXT, false, false);
+                DEFAULT_NETWORK_OFFERING_FOR_KUBERNETES_SERVICE_DISPLAY_TEXT, null, false);
 
         createNetworkOfferingForKubernetes(DEFAULT_NSX_NETWORK_OFFERING_FOR_KUBERNETES_SERVICE_NAME,
-                DEFAULT_NSX_NETWORK_OFFERING_FOR_KUBERNETES_SERVICE_DISPLAY_TEXT, true, false);
+                DEFAULT_NSX_NETWORK_OFFERING_FOR_KUBERNETES_SERVICE_DISPLAY_TEXT, Network.Provider.Nsx, false);
 
         createNetworkOfferingForKubernetes(DEFAULT_NSX_VPC_TIER_NETWORK_OFFERING_FOR_KUBERNETES_SERVICE_NAME,
-                DEFAULT_NSX_VPC_NETWORK_OFFERING_FOR_KUBERNETES_SERVICE_DISPLAY_TEXT , true, true);
+                DEFAULT_NSX_VPC_NETWORK_OFFERING_FOR_KUBERNETES_SERVICE_DISPLAY_TEXT , Network.Provider.Nsx, true);
+
+        createNetworkOfferingForKubernetes(DEFAULT_NETRIS_VPC_TIER_NETWORK_OFFERING_FOR_KUBERNETES_SERVICE_NAME,
+                DEFAULT_NETRIS_VPC_NETWORK_OFFERING_FOR_KUBERNETES_SERVICE_DISPLAY_TEXT , Network.Provider.Netris, true);
 
         getProjectKubernetesAccountRole();
 
@@ -2705,23 +2728,23 @@ public class KubernetesClusterManagerImpl extends ManagerBase implements Kuberne
         return true;
     }
 
-    private void createNetworkOfferingForKubernetes(String offeringName, String offeringDesc, boolean forNsx, boolean forVpc) {
+    private void createNetworkOfferingForKubernetes(String offeringName, String offeringDesc, Network.Provider extNetProvider, boolean forVpc) {
         final Map<Network.Service, Network.Provider> defaultKubernetesServiceNetworkOfferingProviders = new HashMap<Service, Network.Provider>();
         Network.Provider provider = forVpc ? Network.Provider.VPCVirtualRouter : Network.Provider.VirtualRouter;
         defaultKubernetesServiceNetworkOfferingProviders.put(Service.Dhcp, provider);
         defaultKubernetesServiceNetworkOfferingProviders.put(Service.Dns, provider);
         defaultKubernetesServiceNetworkOfferingProviders.put(Service.UserData, provider);
         if (forVpc) {
-            defaultKubernetesServiceNetworkOfferingProviders.put(Service.NetworkACL, forNsx ? Network.Provider.Nsx : provider);
+            defaultKubernetesServiceNetworkOfferingProviders.put(Service.NetworkACL, Objects.requireNonNullElse(extNetProvider, provider));
         } else {
-            defaultKubernetesServiceNetworkOfferingProviders.put(Service.Firewall, forNsx ? Network.Provider.Nsx : provider);
+            defaultKubernetesServiceNetworkOfferingProviders.put(Service.Firewall, Objects.requireNonNullElse(extNetProvider, provider));
         }
-        defaultKubernetesServiceNetworkOfferingProviders.put(Service.Lb, forNsx ? Network.Provider.Nsx : provider);
-        defaultKubernetesServiceNetworkOfferingProviders.put(Service.SourceNat, forNsx ? Network.Provider.Nsx : provider);
-        defaultKubernetesServiceNetworkOfferingProviders.put(Service.StaticNat, forNsx ? Network.Provider.Nsx : provider);
-        defaultKubernetesServiceNetworkOfferingProviders.put(Service.PortForwarding, forNsx ? Network.Provider.Nsx : provider);
+        defaultKubernetesServiceNetworkOfferingProviders.put(Service.Lb, Objects.requireNonNullElse(extNetProvider, provider));
+        defaultKubernetesServiceNetworkOfferingProviders.put(Service.SourceNat, Objects.requireNonNullElse(extNetProvider, provider));
+        defaultKubernetesServiceNetworkOfferingProviders.put(Service.StaticNat, Objects.requireNonNullElse(extNetProvider, provider));
+        defaultKubernetesServiceNetworkOfferingProviders.put(Service.PortForwarding, Objects.requireNonNullElse(extNetProvider, provider));
 
-        if (!forNsx) {
+        if (Objects.isNull(extNetProvider)) {
             defaultKubernetesServiceNetworkOfferingProviders.put(Service.Gateway, Network.Provider.VirtualRouter);
             defaultKubernetesServiceNetworkOfferingProviders.put(Service.Vpn, Network.Provider.VirtualRouter);
         }
@@ -2734,7 +2757,7 @@ public class KubernetesClusterManagerImpl extends ManagerBase implements Kuberne
                         true, false, false, false, false,
                         false, false, false, true, true, false,
                         forVpc, true, false, false);
-        if (forNsx) {
+        if (Objects.nonNull(extNetProvider)) {
             defaultKubernetesServiceNetworkOffering.setNetworkMode(NetworkOffering.NetworkMode.NATTED);
         }
         defaultKubernetesServiceNetworkOffering.setSupportsVmAutoScaling(true);
@@ -2746,7 +2769,7 @@ public class KubernetesClusterManagerImpl extends ManagerBase implements Kuberne
                     new NetworkOfferingServiceMapVO(defaultKubernetesServiceNetworkOffering.getId(), service,
                             defaultKubernetesServiceNetworkOfferingProviders.get(service));
             networkOfferingServiceMapDao.persist(offService);
-            logger.trace("Added service for the network offering: " + offService);
+            logger.trace("Added service for the network offering: {}", offService);
         }
     }
 
diff --git a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterActionWorker.java b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterActionWorker.java
index 1d4b6e8d0a8..6f7579930d7 100644
--- a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterActionWorker.java
+++ b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterActionWorker.java
@@ -79,6 +79,8 @@ import org.apache.commons.io.IOUtils;
 import org.apache.commons.lang3.StringUtils;
 
 import com.cloud.dc.DataCenterVO;
+import com.cloud.dc.Vlan.VlanType;
+import com.cloud.dc.VlanVO;
 import com.cloud.dc.dao.DataCenterDao;
 import com.cloud.dc.dao.VlanDao;
 import com.cloud.exception.InsufficientAddressCapacityException;
@@ -101,6 +103,7 @@ import com.cloud.network.Network;
 import com.cloud.network.NetworkModel;
 import com.cloud.network.NetworkService;
 import com.cloud.network.dao.IPAddressDao;
+import com.cloud.network.dao.IPAddressVO;
 import com.cloud.network.dao.NetworkDao;
 import com.cloud.network.vpc.VpcService;
 import com.cloud.projects.ProjectService;
@@ -227,8 +230,27 @@ public class KubernetesClusterActionWorker {
     protected VirtualMachineTemplate etcdTemplate;
     protected File sshKeyFile;
     protected String publicIpAddress;
+    /** For Netris VPC-tier clusters the Kubernetes API is exposed via the L4 LB rule on the LB IP.
+     *  For all other clusters: same value as {@link #publicIpAddress}. */
+    protected String apiPublicIpAddress;
     protected int sshPort;
 
+    /**
+     * Returns the public IP that Kubernetes nodes should use as the API server cluster endpoint.
+     * For Netris VPC-tier clusters this is the LB IP (stored under {@code PUBLIC_IP_ID}); for all
+     * other clusters it falls back to {@link #publicIpAddress}.
+     * {@link #publicIpAddress} must be set before calling this method.
+     */
+    protected String resolveApiPublicIpAddress(Network network) {
+        if (network != null && network.getVpcId() != null && manager.isNetrisNetwork(network)) {
+            IpAddress lbIp = getVpcTierKubernetesPublicIp(network);
+            if (lbIp != null) {
+                return lbIp.getAddress().addr();
+            }
+        }
+        return publicIpAddress;
+    }
+
 
     protected final String deploySecretsScriptFilename = "deploy-cloudstack-secret";
     protected final String deployProviderScriptFilename = "deploy-provider";
@@ -439,7 +461,11 @@ public class KubernetesClusterActionWorker {
     }
 
     protected IpAddress getVpcTierKubernetesPublicIp(Network network) {
-        KubernetesClusterDetailsVO detailsVO = kubernetesClusterDetailsDao.findDetail(kubernetesCluster.getId(), ApiConstants.PUBLIC_IP_ID);
+        return getVpcTierKubernetesPublicIp(network, ApiConstants.PUBLIC_IP_ID);
+    }
+
+    protected IpAddress getVpcTierKubernetesPublicIp(Network network, String detailKey) {
+        KubernetesClusterDetailsVO detailsVO = kubernetesClusterDetailsDao.findDetail(kubernetesCluster.getId(), detailKey);
         if (detailsVO == null || StringUtils.isEmpty(detailsVO.getValue())) {
             return null;
         }
@@ -474,14 +500,24 @@ public class KubernetesClusterActionWorker {
 
     protected IpAddress acquireVpcTierKubernetesPublicIp(Network network, boolean forEtcd) throws
             InsufficientAddressCapacityException, ResourceAllocationException, ResourceUnavailableException {
-        IpAddress ip = networkService.allocateIP(owner, kubernetesCluster.getZoneId(), network.getId(), null, null);
+        return acquireVpcTierKubernetesPublicIp(network, forEtcd ? null : ApiConstants.PUBLIC_IP_ID);
+    }
+
+    protected IpAddress acquireVpcTierKubernetesPublicIp(Network network, String detailKey) throws
+            InsufficientAddressCapacityException, ResourceAllocationException, ResourceUnavailableException {
+        IpAddress ip;
+        if (manager.isNetrisNetwork(network)) {
+            ip = allocateIpFromNetrisRange(network);
+        } else {
+            ip = networkService.allocateIP(owner, kubernetesCluster.getZoneId(), network.getId(), null, null);
+        }
         if (ip == null) {
             return null;
         }
         ip = vpcService.associateIPToVpc(ip.getId(), network.getVpcId());
         ip = ipAddressManager.associateIPToGuestNetwork(ip.getId(), network.getId(), false);
-        if (!forEtcd) {
-            kubernetesClusterDetailsDao.addDetail(kubernetesCluster.getId(), ApiConstants.PUBLIC_IP_ID, ip.getUuid(), false);
+        if (detailKey != null) {
+            kubernetesClusterDetailsDao.addDetail(kubernetesCluster.getId(), detailKey, ip.getUuid(), false);
         }
         return ip;
     }
@@ -496,6 +532,39 @@ public class KubernetesClusterActionWorker {
         return ip;
     }
 
+    /**
+     * Finds and allocates a free public IP from the Netris-reserved VLAN range in the cluster's
+     * zone. Used for both the LB IP ({@code PUBLIC_IP_ID}) and the NAT IP
+     * ({@code NETRIS_NAT_PUBLIC_IP_ID}): all IPs acquired for a Netris cluster must come from
+     * VLANs tagged {@code forNetris=true} in {@code vlan_details}. The actual allocation is
+     * delegated to {@link NetworkService#allocateIP} with the discovered address so that the
+     * normal account-lock / state-transition path is preserved.
+     *
+     * @return the allocated {@link IpAddress}, or {@code null} if the Netris range is exhausted
+     */
+    private IpAddress allocateIpFromNetrisRange(Network network) throws
+            InsufficientAddressCapacityException, ResourceAllocationException, ResourceUnavailableException {
+        long dcId = kubernetesCluster.getZoneId();
+        List<Long> netrisVlanIds = vlanDao.listVlansForExternalNetworkProvider(dcId, ApiConstants.NETRIS_DETAIL_KEY)
+                .stream().map(VlanVO::getId).collect(Collectors.toList());
+        if (netrisVlanIds.isEmpty()) {
+            throw new CloudRuntimeException(String.format(
+                    "Cannot allocate Netris LB IP for Kubernetes cluster %s: no VLANs tagged " +
+                    "'forNetris=true' found in zone %d", kubernetesCluster.getName(), dcId));
+        }
+        List<IPAddressVO> available = ipAddressManager.listAvailablePublicIps(
+                dcId, null, netrisVlanIds, owner, VlanType.VirtualNetwork,
+                network.getId(), false, false, false, null, null, false,
+                null, null, false, false);
+        if (available.isEmpty()) {
+            logger.warn(String.format("No available IPs in the Netris range for zone %d, " +
+                    "Kubernetes cluster %s", dcId, kubernetesCluster.getName()));
+            return null;
+        }
+        String requestedIp = available.get(0).getAddress().addr();
+        return networkService.allocateIP(owner, dcId, network.getId(), null, requestedIp);
+    }
+
     protected Pair<String, Integer> getKubernetesClusterServerIpSshPortForIsolatedNetwork(Network network) {
         String ip = null;
         IpAddress address = getNetworkSourceNatIp(network);
@@ -519,6 +588,27 @@ public class KubernetesClusterActionWorker {
                                                                                   boolean acquireNewPublicIpForVpcTierIfNeeded) throws
             InsufficientAddressCapacityException, ResourceAllocationException, ResourceUnavailableException {
         int port = CLUSTER_NODES_DEFAULT_START_SSH_PORT;
+        if (manager.isNetrisNetwork(network)) {
+            // For Netris:
+            // NAT IP is the SSH/API/port-forwarding endpoint.
+            // LB IP (PUBLIC_IP_ID) is reserved for Kubernetes LoadBalancer
+            IpAddress natIp = getVpcTierKubernetesPublicIp(network, ApiConstants.NETRIS_NAT_PUBLIC_IP_ID);
+            if (natIp != null) {
+                return new Pair<>(natIp.getAddress().addr(), port);
+            }
+            if (acquireNewPublicIpForVpcTierIfNeeded) {
+                // Acquire LB IP first so it is stored under PUBLIC_IP_ID for Netris
+                if (getVpcTierKubernetesPublicIp(network) == null) {
+                    acquireVpcTierKubernetesPublicIp(network, false);
+                }
+                natIp = acquireVpcTierKubernetesPublicIp(network, ApiConstants.NETRIS_NAT_PUBLIC_IP_ID);
+                if (natIp != null) {
+                    return new Pair<>(natIp.getAddress().addr(), port);
+                }
+            }
+            logger.warn(String.format("No NAT public IP found for Netris VPC tier: %s, Kubernetes cluster: %s", network, kubernetesCluster.getName()));
+            return new Pair<>(null, port);
+        }
         IpAddress address = getVpcTierKubernetesPublicIp(network);
         if (address != null) {
             return new Pair<>(address.getAddress().addr(), port);
diff --git a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterDestroyWorker.java b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterDestroyWorker.java
index 62bd8b4576a..bdf515a9bf4 100644
--- a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterDestroyWorker.java
+++ b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterDestroyWorker.java
@@ -189,12 +189,27 @@ public class KubernetesClusterDestroyWorker extends KubernetesClusterResourceMod
         }
     }
 
-    protected void deleteKubernetesClusterVpcTierRules(Network network, List<Long> removedVmIds) throws ManagementServerException {
+    protected void deleteKubernetesClusterVpcTierRules(Network network, List<Long> removedVmIds) throws ManagementServerException, ResourceUnavailableException {
+        removeVpcTierAclRules(network);
+        if (manager.isNetrisNetwork(network)) {
+            IpAddress lbIp = getVpcTierKubernetesPublicIp(network);
+            if (lbIp != null) {
+                removeLoadBalancingRule(lbIp, network, owner);
+            }
+            IpAddress natIp = getVpcTierKubernetesPublicIp(network, ApiConstants.NETRIS_NAT_PUBLIC_IP_ID);
+            if (natIp != null) {
+                try {
+                    removePortForwardingRules(natIp, network, owner, removedVmIds);
+                } catch (ResourceUnavailableException e) {
+                    throw new ManagementServerException(String.format("Failed to remove Kubernetes cluster port forwarding rules for network : %s", network.getName()), e);
+                }
+            }
+            return;
+        }
         IpAddress publicIp = getVpcTierKubernetesPublicIp(network);
         if (publicIp == null) {
             return;
         }
-        removeVpcTierAclRules(network);
         try {
             removePortForwardingRules(publicIp, network, owner, removedVmIds);
         } catch (ResourceUnavailableException e) {
@@ -202,7 +217,7 @@ public class KubernetesClusterDestroyWorker extends KubernetesClusterResourceMod
         }
     }
 
-    private void deleteKubernetesClusterNetworkRules() throws ManagementServerException {
+    private void deleteKubernetesClusterNetworkRules() throws ManagementServerException, ResourceUnavailableException {
         NetworkVO network = networkDao.findById(kubernetesCluster.getNetworkId());
         if (network == null) {
             return;
@@ -242,7 +257,7 @@ public class KubernetesClusterDestroyWorker extends KubernetesClusterResourceMod
         }
     }
 
-    private void checkForRulesToDelete() throws ManagementServerException {
+    private void checkForRulesToDelete() throws ManagementServerException, ResourceUnavailableException {
         NetworkVO kubernetesClusterNetwork = networkDao.findById(kubernetesCluster.getNetworkId());
         if (kubernetesClusterNetwork != null && !manager.isDirectAccess(kubernetesClusterNetwork)) {
             deleteKubernetesClusterNetworkRules();
@@ -254,12 +269,18 @@ public class KubernetesClusterDestroyWorker extends KubernetesClusterResourceMod
         if (networkVO == null || networkVO.getVpcId() == null) {
             return;
         }
-        IpAddress address = getVpcTierKubernetesPublicIp(networkVO);
-        if (address == null) {
-            return;
+        IpAddress lbIp = getVpcTierKubernetesPublicIp(networkVO);
+        if (lbIp != null) {
+            networkService.releaseIpAddress(lbIp.getId());
+            kubernetesClusterDetailsDao.removeDetail(kubernetesCluster.getId(), ApiConstants.PUBLIC_IP_ID);
+        }
+        if (manager.isNetrisNetwork(networkVO)) {
+            IpAddress natIp = getVpcTierKubernetesPublicIp(networkVO, ApiConstants.NETRIS_NAT_PUBLIC_IP_ID);
+            if (natIp != null) {
+                networkService.releaseIpAddress(natIp.getId());
+                kubernetesClusterDetailsDao.removeDetail(kubernetesCluster.getId(), ApiConstants.NETRIS_NAT_PUBLIC_IP_ID);
+            }
         }
-        networkService.releaseIpAddress(address.getId());
-        kubernetesClusterDetailsDao.removeDetail(kubernetesCluster.getId(), ApiConstants.PUBLIC_IP_ID);
     }
 
     public boolean destroy() throws CloudRuntimeException {
@@ -324,8 +345,8 @@ public class KubernetesClusterDestroyWorker extends KubernetesClusterResourceMod
             } else {
                 try {
                     checkForRulesToDelete();
-                } catch (ManagementServerException e) {
-                    String msg = String.format("Failed to remove network rules of Kubernetes cluster: %s", kubernetesCluster);
+                } catch (ManagementServerException | ResourceUnavailableException e) {
+                    String msg = String.format("Failed to remove network rules of Kubernetes cluster : %s", kubernetesCluster.getName());
                     logger.warn(msg, e);
                     updateKubernetesClusterEntryForGC();
                     throw new CloudRuntimeException(msg, e);
diff --git a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterResourceModifierActionWorker.java b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterResourceModifierActionWorker.java
index 55924cb32e8..573e752ce12 100644
--- a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterResourceModifierActionWorker.java
+++ b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterResourceModifierActionWorker.java
@@ -799,6 +799,29 @@ public class KubernetesClusterResourceModifierActionWorker extends KubernetesClu
         }
     }
 
+
+    /**
+     *
+     * Netris-specific VPC tier rule setup. The two IPs serve distinct roles in Netris:
+     *   lbIp  (PUBLIC_IP_ID)            — Kubernetes API (port 6443) via Netris L4 Load Balancer rule.
+     *   natIp (NETRIS_NAT_PUBLIC_IP_ID) — SSH node access (ports 22001+) via Netris DNAT / port-forwarding.
+     */
+    protected void setupKubernetesClusterVpcTierRulesForNetris(IpAddress lbIp, IpAddress natIp,
+            Network network, List<Long> clusterVMIds) throws ManagementServerException {
+        createVpcTierAclRules(network);
+        try {
+            provisionLoadBalancerRule(lbIp, network, owner, clusterVMIds, CLUSTER_API_PORT);
+        } catch (InsufficientAddressCapacityException | NetworkRuleConflictException e) {
+            throw new ManagementServerException(String.format("Failed to activate API load balancing rules for Netris Kubernetes cluster : %s", kubernetesCluster.getName()), e);
+        }
+        try {
+            Map<Long, Integer> vmIdPortMap = getVmPortMap();
+            provisionSshPortForwardingRules(natIp, network, owner, clusterVMIds, vmIdPortMap);
+        } catch (ResourceUnavailableException | NetworkRuleConflictException e) {
+            throw new ManagementServerException(String.format("Failed to activate SSH port forwarding rules for Netris Kubernetes cluster : %s", kubernetesCluster.getName()), e);
+        }
+    }
+
     /**
      * Generates a valid name prefix for Kubernetes cluster nodes.
      *
diff --git a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterScaleWorker.java b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterScaleWorker.java
index 0ca27c29d8d..8f8d3aff87e 100644
--- a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterScaleWorker.java
+++ b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterScaleWorker.java
@@ -32,6 +32,7 @@ import com.cloud.kubernetes.cluster.KubernetesServiceHelper.KubernetesClusterNod
 import com.cloud.service.ServiceOfferingVO;
 import com.cloud.storage.VMTemplateVO;
 import org.apache.cloudstack.api.ApiCommandResourceType;
+import org.apache.cloudstack.api.ApiConstants;
 import org.apache.cloudstack.api.InternalIdentity;
 import org.apache.cloudstack.context.CallContext;
 import org.apache.commons.collections.CollectionUtils;
@@ -156,6 +157,25 @@ public class KubernetesClusterScaleWorker extends KubernetesClusterResourceModif
     }
 
     private void scaleKubernetesClusterVpcTierRules(final List<Long> clusterVMIds) throws ManagementServerException {
+        if (manager.isNetrisNetwork(network)) {
+            // For Netris: SSH port-forwarding is on NAT IP; the LB rule on LB IP for 6443 is fixed and never re-provisioned during scale
+            IpAddress natIp = getVpcTierKubernetesPublicIp(network, ApiConstants.NETRIS_NAT_PUBLIC_IP_ID);
+            if (natIp == null) {
+                throw new ManagementServerException(String.format("No NAT IP found for Netris VPC tier : %s, Kubernetes cluster : %s", network.getName(), kubernetesCluster.getName()));
+            }
+            try {
+                removePortForwardingRules(natIp, network, owner, CLUSTER_NODES_DEFAULT_START_SSH_PORT, CLUSTER_NODES_DEFAULT_START_SSH_PORT + clusterVMIds.size() - 1);
+            } catch (ResourceUnavailableException e) {
+                throw new ManagementServerException(String.format("Failed to remove SSH port forwarding rules for the Kubernetes cluster : %s", kubernetesCluster.getName()), e);
+            }
+            try {
+                Map<Long, Integer> vmIdPortMap = getVmPortMap();
+                provisionSshPortForwardingRules(natIp, network, owner, clusterVMIds, vmIdPortMap);
+            } catch (ResourceUnavailableException | NetworkRuleConflictException e) {
+                throw new ManagementServerException(String.format("Failed to activate SSH port forwarding rules for the Kubernetes cluster : %s", kubernetesCluster.getName()), e);
+            }
+            return;
+        }
         IpAddress publicIp = getVpcTierKubernetesPublicIp(network);
         if (publicIp == null) {
             throw new ManagementServerException(String.format("No public IP addresses found for VPC tier : %s, Kubernetes cluster : %s", network.getName(), kubernetesCluster.getName()));
@@ -165,7 +185,6 @@ public class KubernetesClusterScaleWorker extends KubernetesClusterResourceModif
         } catch (ResourceUnavailableException e) {
             throw new ManagementServerException(String.format("Failed to remove SSH port forwarding rules for removed VMs for the Kubernetes cluster : %s", kubernetesCluster.getName()), e);
         }
-        // Add port forwarding rule for SSH access on each node VM
         try {
            Map<Long, Integer> vmIdPortMap = getVmPortMap();
             provisionSshPortForwardingRules(publicIp, network, owner, clusterVMIds, vmIdPortMap);
@@ -479,7 +498,7 @@ public class KubernetesClusterScaleWorker extends KubernetesClusterResourceModif
             launchPermissionDao.persist(launchPermission);
         }
         try {
-            clusterVMs = provisionKubernetesClusterNodeVms((int)(newVmCount + kubernetesCluster.getNodeCount()), (int)kubernetesCluster.getNodeCount(), publicIpAddress, kubernetesCluster.getDomainId(), kubernetesCluster.getAccountId());
+            clusterVMs = provisionKubernetesClusterNodeVms((int)(newVmCount + kubernetesCluster.getNodeCount()), (int)kubernetesCluster.getNodeCount(), apiPublicIpAddress, kubernetesCluster.getDomainId(), kubernetesCluster.getAccountId());
             updateLoginUserDetails(clusterVMs.stream().map(InternalIdentity::getId).collect(Collectors.toList()));
         } catch (CloudRuntimeException | ManagementServerException | ResourceUnavailableException | InsufficientCapacityException e) {
             logTransitStateToFailedIfNeededAndThrow(Level.ERROR, String.format("Scaling failed for Kubernetes cluster : %s, unable to provision node VM in the cluster", kubernetesCluster.getName()), e);
@@ -518,6 +537,9 @@ public class KubernetesClusterScaleWorker extends KubernetesClusterResourceModif
         Pair<String, Integer> publicIpSshPort = getKubernetesClusterServerIpSshPort(null);
         publicIpAddress = publicIpSshPort.first();
         sshPort = publicIpSshPort.second();
+        // For Netris VPC-tier: apiPublicIpAddress = LB IP (kubeadm join target for new nodes).
+        // For all others: same as publicIpAddress.
+        apiPublicIpAddress = resolveApiPublicIpAddress(network);
         if (StringUtils.isEmpty(publicIpAddress)) {
             logTransitStateToFailedIfNeededAndThrow(Level.ERROR, String.format("Scaling failed for Kubernetes cluster : %s, unable to retrieve associated public IP", kubernetesCluster.getName()));
         }
diff --git a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterStartWorker.java b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterStartWorker.java
index aa9317e619b..4b7f3c67225 100644
--- a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterStartWorker.java
+++ b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterStartWorker.java
@@ -38,6 +38,7 @@ import com.cloud.kubernetes.cluster.KubernetesServiceHelper;
 import com.cloud.network.vpc.NetworkACL;
 import com.cloud.storage.VMTemplateVO;
 import com.cloud.user.UserDataVO;
+import org.apache.cloudstack.api.ApiConstants;
 import org.apache.cloudstack.api.BaseCmd;
 import org.apache.cloudstack.api.InternalIdentity;
 import org.apache.cloudstack.framework.ca.Certificate;
@@ -103,6 +104,31 @@ public class KubernetesClusterStartWorker extends KubernetesClusterResourceModif
         return kubernetesClusterVersion;
     }
 
+    /**
+     * Returns the VPC-tier public IP stored under {@code detailKey} for this cluster,
+     * acquiring a new one if absent. Throws {@link ManagementServerException} when
+     * neither the lookup nor the allocation succeeds.
+     */
+    private IpAddress resolveOrAcquireVpcTierIp(Network network, String detailKey, String role)
+            throws ManagementServerException, InsufficientAddressCapacityException, ResourceUnavailableException {
+        IpAddress ip = getVpcTierKubernetesPublicIp(network, detailKey);
+        if (ip == null) {
+            try {
+                ip = acquireVpcTierKubernetesPublicIp(network, detailKey);
+            } catch (ResourceAllocationException e) {
+                throw new ManagementServerException(String.format(
+                        "Failed to acquire %s IP for VPC tier : %s, Kubernetes cluster : %s",
+                        role, network.getName(), kubernetesCluster.getName()), e);
+            }
+        }
+        if (ip == null) {
+            throw new ManagementServerException(String.format(
+                    "Failed to acquire %s IP for VPC tier : %s, Kubernetes cluster : %s",
+                    role, network.getName(), kubernetesCluster.getName()));
+        }
+        return ip;
+    }
+
     private Pair<String, Map<Long, Network.IpAddresses>> getKubernetesControlNodeIpAddresses(final DataCenter zone, final Network network, final Account account) throws InsufficientAddressCapacityException {
         String controlNodeIp = null;
         Map<Long, Network.IpAddresses> requestedIps = null;
@@ -608,7 +634,7 @@ public class KubernetesClusterStartWorker extends KubernetesClusterResourceModif
         return network;
     }
 
-    protected void setupKubernetesClusterNetworkRules(Network network, List<UserVm> clusterVMs) throws ManagementServerException {
+    protected void setupKubernetesClusterNetworkRules(Network network, List<UserVm> clusterVMs) throws ManagementServerException, InsufficientAddressCapacityException, ResourceUnavailableException {
         if (manager.isDirectAccess(network)) {
             if (logger.isDebugEnabled()) {
                 logger.debug("Network: {} for Kubernetes cluster: {} is not an isolated network or ROUTED network, therefore, no need for network rules", network, kubernetesCluster);
@@ -617,11 +643,14 @@ public class KubernetesClusterStartWorker extends KubernetesClusterResourceModif
         }
         List<Long> clusterVMIds = clusterVMs.stream().map(UserVm::getId).collect(Collectors.toList());
         if (network.getVpcId() != null) {
-            IpAddress publicIp = getVpcTierKubernetesPublicIp(network);
-            if (publicIp == null) {
-                throw new ManagementServerException(String.format("No public IP addresses found for VPC tier : %s, Kubernetes cluster : %s", network.getName(), kubernetesCluster.getName()));
+            if (manager.isNetrisNetwork(network)) {
+                IpAddress natIp = resolveOrAcquireVpcTierIp(network, ApiConstants.NETRIS_NAT_PUBLIC_IP_ID, "NAT");
+                IpAddress lbIp = resolveOrAcquireVpcTierIp(network, ApiConstants.PUBLIC_IP_ID, "LB");
+                setupKubernetesClusterVpcTierRulesForNetris(lbIp, natIp, network, clusterVMIds);
+            } else {
+                IpAddress lbIp = resolveOrAcquireVpcTierIp(network, ApiConstants.PUBLIC_IP_ID, "public");
+                setupKubernetesClusterVpcTierRules(lbIp, network, clusterVMIds);
             }
-            setupKubernetesClusterVpcTierRules(publicIp, network, clusterVMIds);
             return;
         }
         IpAddress publicIp = getNetworkSourceNatIp(network);
@@ -711,7 +740,7 @@ public class KubernetesClusterStartWorker extends KubernetesClusterResourceModif
             final String controlVMPrivateIpAddress = getControlVmPrivateIp();
             if (StringUtils.isNotEmpty(controlVMPrivateIpAddress)) {
                 kubeConfig = kubeConfig.replace(String.format("server: https://%s:%d", controlVMPrivateIpAddress, CLUSTER_API_PORT),
-                        String.format("server: https://%s:%d", publicIpAddress, CLUSTER_API_PORT));
+                        String.format("server: https://%s:%d", apiPublicIpAddress, CLUSTER_API_PORT));
             }
             kubernetesClusterDetailsDao.addDetail(kubernetesCluster.getId(), "kubeConfigData", Base64.encodeBase64String(kubeConfig.getBytes(com.cloud.utils.StringUtils.getPreferredCharset())), false);
             return true;
@@ -735,7 +764,7 @@ public class KubernetesClusterStartWorker extends KubernetesClusterResourceModif
 
     private void updateKubernetesClusterEntryEndpoint() {
         KubernetesClusterVO kubernetesClusterVO = kubernetesClusterDao.findById(kubernetesCluster.getId());
-        kubernetesClusterVO.setEndpoint(String.format("https://%s:%d/", publicIpAddress, CLUSTER_API_PORT));
+        kubernetesClusterVO.setEndpoint(String.format("https://%s:%d/", apiPublicIpAddress, CLUSTER_API_PORT));
         kubernetesClusterDao.update(kubernetesCluster.getId(), kubernetesClusterVO);
     }
 
@@ -767,6 +796,7 @@ public class KubernetesClusterStartWorker extends KubernetesClusterResourceModif
             logTransitStateAndThrow(Level.ERROR, String.format("Failed to start Kubernetes cluster : %s as failed to acquire public IP" , kubernetesCluster.getName()), kubernetesCluster.getId(), KubernetesCluster.Event.CreateFailed);
         }
         publicIpAddress = publicIpSshPort.first();
+        apiPublicIpAddress = resolveApiPublicIpAddress(network);
         if (StringUtils.isEmpty(publicIpAddress) &&
                 (!manager.isDirectAccess(network) || kubernetesCluster.getControlNodeCount() > 1)) { // Shared network, single-control node cluster won't have an IP yet
             logTransitStateAndThrow(Level.ERROR, String.format("Failed to start Kubernetes cluster : %s as no public IP found for the cluster" , kubernetesCluster.getName()), kubernetesCluster.getId(), KubernetesCluster.Event.CreateFailed);
@@ -789,7 +819,7 @@ public class KubernetesClusterStartWorker extends KubernetesClusterResourceModif
         Pair<UserVm, String> k8sControlVMAndIp = new Pair<>(null, null);
         UserVm k8sControlVM = null;
         try {
-            k8sControlVMAndIp = provisionKubernetesClusterControlVm(network, publicIpAddress, etcdGuestNodeIps, domainId, accountId, asNumber);
+            k8sControlVMAndIp = provisionKubernetesClusterControlVm(network, apiPublicIpAddress, etcdGuestNodeIps, domainId, accountId, asNumber);
         } catch (CloudRuntimeException | ManagementServerException | ResourceUnavailableException | InsufficientCapacityException e) {
             logTransitStateAndThrow(Level.ERROR, String.format("Provisioning the control VM failed in the Kubernetes cluster : %s", kubernetesCluster.getName()), kubernetesCluster.getId(), KubernetesCluster.Event.CreateFailed, e);
         }
@@ -798,6 +828,7 @@ public class KubernetesClusterStartWorker extends KubernetesClusterResourceModif
         if (StringUtils.isEmpty(publicIpAddress)) {
             publicIpSshPort = getKubernetesClusterServerIpSshPort(k8sControlVM);
             publicIpAddress = publicIpSshPort.first();
+            apiPublicIpAddress = resolveApiPublicIpAddress(network);
             if (StringUtils.isEmpty(publicIpAddress)) {
                 logTransitStateAndThrow(Level.WARN, String.format("Failed to start Kubernetes cluster : %s as no public IP found for the cluster", kubernetesCluster.getName()), kubernetesCluster.getId(), KubernetesCluster.Event.CreateFailed);
             }
@@ -819,7 +850,7 @@ public class KubernetesClusterStartWorker extends KubernetesClusterResourceModif
         }
         try {
             setupKubernetesClusterNetworkRules(network, clusterVMs);
-        } catch (ManagementServerException e) {
+        } catch (ManagementServerException | InsufficientAddressCapacityException | ResourceUnavailableException e) {
             logTransitStateAndThrow(Level.ERROR, String.format("Failed to setup Kubernetes cluster : %s, unable to setup network rules", kubernetesCluster.getName()), kubernetesCluster.getId(), KubernetesCluster.Event.CreateFailed, e);
         }
         try {
@@ -840,7 +871,7 @@ public class KubernetesClusterStartWorker extends KubernetesClusterResourceModif
             }
             logTransitStateDetachIsoAndThrow(Level.ERROR, msg, kubernetesCluster, clusterVMs, KubernetesCluster.Event.CreateFailed, null);
         }
-        boolean k8sApiServerSetup = KubernetesClusterUtil.isKubernetesClusterServerRunning(kubernetesCluster, publicIpAddress, CLUSTER_API_PORT, startTimeoutTime, 15000);
+        boolean k8sApiServerSetup = KubernetesClusterUtil.isKubernetesClusterServerRunning(kubernetesCluster, apiPublicIpAddress, CLUSTER_API_PORT, startTimeoutTime, 15000);
         if (!k8sApiServerSetup) {
             logTransitStateDetachIsoAndThrow(Level.ERROR, String.format("Failed to setup Kubernetes cluster : %s in usable state as unable to provision API endpoint for the cluster", kubernetesCluster.getName()), kubernetesCluster, clusterVMs, KubernetesCluster.Event.CreateFailed, null);
         }
@@ -886,10 +917,13 @@ public class KubernetesClusterStartWorker extends KubernetesClusterResourceModif
         Pair<String, Integer> sshIpPort =  getKubernetesClusterServerIpSshPort(null);
         publicIpAddress = sshIpPort.first();
         sshPort = sshIpPort.second();
+        Network network = networkDao.findById(kubernetesCluster.getNetworkId());
+        apiPublicIpAddress = resolveApiPublicIpAddress(network);
         if (StringUtils.isEmpty(publicIpAddress)) {
             logTransitStateAndThrow(Level.ERROR, String.format("Failed to start Kubernetes cluster : %s as no public IP found for the cluster" , kubernetesCluster.getName()), kubernetesCluster.getId(), KubernetesCluster.Event.OperationFailed);
         }
-        if (!KubernetesClusterUtil.isKubernetesClusterServerRunning(kubernetesCluster, publicIpAddress, CLUSTER_API_PORT, startTimeoutTime, 15000)) {
+        // API check uses apiPublicIpAddress (LB IP for Netris)
+        if (!KubernetesClusterUtil.isKubernetesClusterServerRunning(kubernetesCluster, apiPublicIpAddress, CLUSTER_API_PORT, startTimeoutTime, 15000)) {
             logTransitStateAndThrow(Level.ERROR, String.format("Failed to start Kubernetes cluster : %s in usable state", kubernetesCluster.getName()), kubernetesCluster.getId(), KubernetesCluster.Event.OperationFailed);
         }
         if (!isKubernetesClusterKubeConfigAvailable(startTimeoutTime)) {
@@ -915,6 +949,8 @@ public class KubernetesClusterStartWorker extends KubernetesClusterResourceModif
         Pair<String, Integer> sshIpPort =  getKubernetesClusterServerIpSshPort(null);
         publicIpAddress = sshIpPort.first();
         sshPort = sshIpPort.second();
+        Network network = networkDao.findById(kubernetesCluster.getNetworkId());
+        apiPublicIpAddress = resolveApiPublicIpAddress(network);
         if (StringUtils.isEmpty(publicIpAddress)) {
             return false;
         }
@@ -930,7 +966,8 @@ public class KubernetesClusterStartWorker extends KubernetesClusterResourceModif
         if (StringUtils.isEmpty(sshIpPort.first())) {
             return false;
         }
-        if (!KubernetesClusterUtil.isKubernetesClusterServerRunning(kubernetesCluster, sshIpPort.first(),
+        // API server check uses apiPublicIpAddress (LB IP for Netris)
+        if (!KubernetesClusterUtil.isKubernetesClusterServerRunning(kubernetesCluster, apiPublicIpAddress,
                 KubernetesClusterActionWorker.CLUSTER_API_PORT, startTimeoutTime, 0)) {
             return false;
         }
diff --git a/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/agent/api/CreateNetrisVnetCommand.java b/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/agent/api/CreateNetrisVnetCommand.java
index e700b3b9d2d..dfca9bade7d 100644
--- a/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/agent/api/CreateNetrisVnetCommand.java
+++ b/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/agent/api/CreateNetrisVnetCommand.java
@@ -16,6 +16,9 @@
 // under the License.
 package org.apache.cloudstack.agent.api;
 
+import com.cloud.offering.NetworkOffering;
+import com.cloud.utils.net.NetUtils;
+
 public class CreateNetrisVnetCommand extends NetrisCommand {
     private String vpcName;
     private Long vpcId;
@@ -24,7 +27,8 @@ public class CreateNetrisVnetCommand extends NetrisCommand {
     private String gateway;
     private String netrisTag;
     private String ipv6Cidr;
-    private Boolean globalRouting;
+    private NetworkOffering.NetworkMode networkMode;
+    private NetUtils.InternetProtocol internetProtocol;
 
     public CreateNetrisVnetCommand(Long zoneId, Long accountId, Long domainId, String vpcName, Long vpcId, String vNetName, Long networkId, String cidr, String gateway, boolean isVpc) {
         super(zoneId, accountId, domainId, vNetName, networkId, isVpc);
@@ -74,11 +78,19 @@ public class CreateNetrisVnetCommand extends NetrisCommand {
         this.ipv6Cidr = ipv6Cidr;
     }
 
-    public Boolean isGlobalRouting() {
-        return globalRouting;
+    public NetworkOffering.NetworkMode getNetworkMode() {
+        return networkMode;
+    }
+
+    public void setNetworkMode(NetworkOffering.NetworkMode networkMode) {
+        this.networkMode = networkMode;
+    }
+
+    public NetUtils.InternetProtocol getInternetProtocol() {
+        return internetProtocol;
     }
 
-    public void setGlobalRouting(Boolean globalRouting) {
-        this.globalRouting = globalRouting;
+    public void setInternetProtocol(NetUtils.InternetProtocol internetProtocol) {
+        this.internetProtocol = internetProtocol;
     }
 }
diff --git a/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/agent/api/NetrisCommand.java b/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/agent/api/NetrisCommand.java
index 280c7de32d1..3c14ed764f1 100644
--- a/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/agent/api/NetrisCommand.java
+++ b/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/agent/api/NetrisCommand.java
@@ -25,6 +25,7 @@ public class NetrisCommand extends Command {
     private String name;
     private final Long id;
     private final boolean isVpc;
+    private boolean isL2;
 
     public NetrisCommand(long zoneId, Long accountId, Long domainId, String name, Long id, boolean isVpc) {
         this.zoneId = zoneId;
@@ -63,6 +64,14 @@ public class NetrisCommand extends Command {
         return isVpc;
     }
 
+    public boolean isL2() {
+        return isL2;
+    }
+
+    public void setL2(boolean l2) {
+        isL2 = l2;
+    }
+
     @Override
     public boolean executeInSequence() {
         return false;
diff --git a/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/resource/NetrisResourceObjectUtils.java b/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/resource/NetrisResourceObjectUtils.java
index 164c56cd763..d67054398f6 100644
--- a/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/resource/NetrisResourceObjectUtils.java
+++ b/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/resource/NetrisResourceObjectUtils.java
@@ -16,6 +16,7 @@
 // under the License.
 package org.apache.cloudstack.resource;
 
+import com.cloud.offering.NetworkOffering;
 import org.apache.cloudstack.agent.api.NetrisCommand;
 import org.apache.commons.lang3.ArrayUtils;
 
@@ -73,7 +74,7 @@ public class NetrisResourceObjectUtils {
                 break;
             case STATICNAT:
                 stringBuilder.append(String.format("%s%s-VM%s-%s", prefix, suffixes[0], suffixes[1], "STATICNAT"));
-                suffixes = new String[0];
+                suffixes = ArrayUtils.subarray(suffixes, 2, suffixes.length);
                 break;
             case DNAT:
                 stringBuilder.append(String.format("%s%s-%s", prefix, suffixes[0], "DNAT"));
@@ -100,4 +101,8 @@ public class NetrisResourceObjectUtils {
         }
         return stringBuilder.toString();
     }
+
+    public static NetworkOffering.NetworkMode getNetworkMode(NetworkOffering.NetworkMode mode) {
+        return Objects.isNull(mode) ? NetworkOffering.NetworkMode.NATTED : mode;
+    }
 }
diff --git a/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/service/NetrisApiClientImpl.java b/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/service/NetrisApiClientImpl.java
index 9d3ff953ee1..c4598ddfec4 100644
--- a/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/service/NetrisApiClientImpl.java
+++ b/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/service/NetrisApiClientImpl.java
@@ -21,6 +21,7 @@ import com.cloud.network.netris.NetrisLbBackend;
 import com.cloud.network.netris.NetrisNetworkRule;
 import com.cloud.network.vpc.StaticRoute;
 import com.cloud.network.vpc.StaticRouteVO;
+import com.cloud.offering.NetworkOffering;
 import com.cloud.user.Account;
 import com.cloud.utils.Pair;
 import com.cloud.utils.exception.CloudRuntimeException;
@@ -108,6 +109,7 @@ import io.netris.model.VnetResAddBody;
 import io.netris.model.VnetResDeleteBody;
 import io.netris.model.VnetResListBody;
 import io.netris.model.VnetsBody;
+import io.netris.model.VnetsBodyGateways;
 import io.netris.model.VpcEditResponseOK;
 import io.netris.model.VpcVpcIdBody;
 import io.netris.model.response.AuthResponse;
@@ -329,6 +331,11 @@ public class NetrisApiClientImpl implements NetrisApiClient {
     @Override
     public boolean createVpc(CreateNetrisVpcCommand cmd) {
         String netrisVpcName = NetrisResourceObjectUtils.retrieveNetrisResourceObjectName(cmd, NetrisResourceObjectUtils.NetrisObjectType.VPC);
+        VPCListing existingNetrisVpc = getVpcByNameAndTenant(netrisVpcName);
+        if (existingNetrisVpc != null) {
+            logger.info("Netris VPC {} already exists, skipping creation", netrisVpcName);
+            return true;
+        }
         VPCResponseObjectOK createdVpc = createVpcInternal(netrisVpcName, tenantId, tenantName);
         if (createdVpc == null || !createdVpc.isIsSuccess()) {
             String reason = createdVpc == null ? "Empty response" : "Operation failed on Netris";
@@ -352,9 +359,13 @@ public class NetrisApiClientImpl implements NetrisApiClient {
         String netrisVpcName = NetrisResourceObjectUtils.retrieveNetrisResourceObjectName(cmd, NetrisResourceObjectUtils.NetrisObjectType.VPC);
         String netrisPrevVpcName = String.format("D%s-A%s-Z%s-V%s-%s", domainId, accountId, zoneId, vpcId, prevVpcName);
         VpcEditResponseOK updatedVpc = updateVpcInternal(netrisVpcName, netrisPrevVpcName, tenantId, tenantName);
+        return validateNetrisVpcUpdateResponse(updatedVpc, prevVpcName);
+    }
+
+    private boolean validateNetrisVpcUpdateResponse(VpcEditResponseOK updatedVpc, String previousVpcName) {
         if (updatedVpc == null || !updatedVpc.isIsSuccess()) {
             String reason = updatedVpc == null ? "Empty response" : "Operation failed on Netris";
-            logger.debug("The update of Netris VPC {} failed: {}", cmd.getPreviousVpcName(), reason);
+            logger.debug("The update of Netris VPC {} failed: {}", previousVpcName, reason);
             return false;
         }
         return true;
@@ -372,6 +383,17 @@ public class NetrisApiClientImpl implements NetrisApiClient {
             }
             String natRuleName = cmd.getNatRuleName();
             NatGetBody existingNatRule = netrisNatRuleExists(natRuleName);
+            // Backward compatibility: rules created before the public-IP suffix was added use the legacy name
+            if (existingNatRule == null && "STATICNAT".equals(cmd.getNatRuleType())) {
+                String legacyName = getLegacyStaticNatRuleName(natRuleName);
+                if (legacyName != null) {
+                    logger.debug("Static NAT rule not found with name '{}', falling back to legacy name '{}'", natRuleName, legacyName);
+                    existingNatRule = netrisNatRuleExists(legacyName);
+                    if (existingNatRule != null) {
+                        natRuleName = legacyName;
+                    }
+                }
+            }
             boolean ruleExists = Objects.nonNull(existingNatRule);
             if (ruleExists) {
                 deleteNatRule(natRuleName, existingNatRule.getId(), vpcResource.getName());
@@ -1104,6 +1126,32 @@ public class NetrisApiClientImpl implements NetrisApiClient {
         return null;
     }
 
+    private String getL2VpcName(NetrisCommand cmd, String name) {
+        return String.format("D%s-A%s-Z%s-N%s-%s",
+                cmd.getDomainId(), cmd.getAccountId(), cmd.getZoneId(), cmd.getId(), name);
+    }
+
+    /**
+     * Returns the existing dedicated Netris VPC for an L2 network, creating it if it does not yet exist.
+     * The VPC is created with no IPAM allocation.
+     */
+    private VPCListing getOrCreateL2Vpc(NetrisCommand cmd) {
+        String l2VpcName = getL2VpcName(cmd, cmd.getName());
+        VPCListing existing = getVpcByNameAndTenant(l2VpcName);
+        if (existing != null) {
+            logger.debug("Found existing dedicated L2 VPC {} on Netris", l2VpcName);
+            return existing;
+        }
+        logger.debug("Creating dedicated L2 VPC {} on Netris (no subnet)", l2VpcName);
+        VPCResponseObjectOK created = createVpcInternal(l2VpcName, tenantId, tenantName);
+        if (created == null || !created.isIsSuccess()) {
+            logger.error("Failed to create dedicated L2 VPC {} on Netris", l2VpcName);
+            return null;
+        }
+
+        return getVpcByNameAndTenant(l2VpcName);
+    }
+
     private VPCListing getVpcByNameAndTenant(String vpcName) {
         try {
             List<VPCListing> vpcListings = listVPCs();
@@ -1159,18 +1207,47 @@ public class NetrisApiClientImpl implements NetrisApiClient {
         String vnetCidr = cmd.getCidr();
         Integer vxlanId = cmd.getVxlanId();
         String netrisTag = cmd.getNetrisTag();
-        String netmask = vnetCidr.split("/")[1];
-        String netrisGateway = cmd.getGateway() + "/" + netmask;
-        String netrisV6Cidr = cmd.getIpv6Cidr();
         boolean isVpc = cmd.isVpc();
-        Boolean isGlobalRouting = cmd.isGlobalRouting();
+        boolean isL2 = cmd.isL2();
+
+        // Derive per-address-family global routing flags from networkMode + internetProtocol:
+        //   ipv4 NAT      -> ipv4 global routing: false,  ipv6 global routing: n/a
+        //   ipv4 ROUTED   -> ipv4 global routing: true,   ipv6 global routing: n/a
+        //   dual NAT      -> ipv4 global routing: false,  ipv6 global routing: true
+        //   dual ROUTED   -> ipv4 global routing: true,   ipv6 global routing: true
+        NetworkOffering.NetworkMode networkMode = cmd.getNetworkMode();
+        NetUtils.InternetProtocol internetProtocol = cmd.getInternetProtocol();
+        Boolean ipv4GlobalRouting = null;
+        Boolean ipv6GlobalRouting = null;
+        if (networkMode != null) {
+            ipv4GlobalRouting = NetworkOffering.NetworkMode.ROUTED.equals(networkMode);
+            if (NetUtils.InternetProtocol.DualStack.equals(internetProtocol)) {
+                ipv6GlobalRouting = true;
+            }
+        }
+
+        String netrisGateway = null;
+        if (!isL2 && vnetCidr != null && cmd.getGateway() != null) {
+            String netmask = vnetCidr.split("/")[1];
+            netrisGateway = cmd.getGateway() + "/" + netmask;
+        }
+        String netrisV6Cidr = cmd.getIpv6Cidr();
 
         try {
-            String netrisVpcName = getNetrisVpcName(cmd, vpcId, vpcName);
-            VPCListing associatedVpc = getNetrisVpcResource(netrisVpcName);
-            if (associatedVpc == null) {
-                logger.error("Failed to find Netris VPC with name: {}, to create the corresponding vNet for network {}", netrisVpcName, networkName);
-                return false;
+            VPCListing associatedVpc;
+            if (isL2) {
+                associatedVpc = getOrCreateL2Vpc(cmd);
+                if (associatedVpc == null) {
+                    logger.error("Failed to get or create dedicated L2 VPC to create the corresponding vNet for L2 network {}", networkName);
+                    return false;
+                }
+            } else {
+                String netrisVpcName = getNetrisVpcName(cmd, vpcId, vpcName);
+                associatedVpc = getNetrisVpcResource(netrisVpcName);
+                if (associatedVpc == null) {
+                    logger.error("Failed to find Netris VPC with name: {}, to create the corresponding vNet for network {}", netrisVpcName, networkName);
+                    return false;
+                }
             }
 
             String vNetName;
@@ -1179,25 +1256,27 @@ public class NetrisApiClientImpl implements NetrisApiClient {
             } else {
                 vNetName = String.format("N%s-%s", networkId, networkName);
             }
-            String netrisVnetName = NetrisResourceObjectUtils.retrieveNetrisResourceObjectName(cmd, NetrisResourceObjectUtils.NetrisObjectType.VNET, vNetName) ;
-            String netrisSubnetName = NetrisResourceObjectUtils.retrieveNetrisResourceObjectName(cmd, NetrisResourceObjectUtils.NetrisObjectType.IPAM_SUBNET, String.valueOf(cmd.getVpcId()), vnetCidr) ;
-
-            createIpamSubnetInternal(netrisSubnetName, vnetCidr, SubnetBody.PurposeEnum.COMMON, associatedVpc, isGlobalRouting);
-            if (Objects.nonNull(netrisV6Cidr)) {
-                String netrisV6IpamAllocationName = NetrisResourceObjectUtils.retrieveNetrisResourceObjectName(cmd, NetrisResourceObjectUtils.NetrisObjectType.IPAM_ALLOCATION, netrisV6Cidr);
-                String netrisV6SubnetName = NetrisResourceObjectUtils.retrieveNetrisResourceObjectName(cmd, NetrisResourceObjectUtils.NetrisObjectType.IPAM_SUBNET,  String.valueOf(cmd.getVpcId()), netrisV6Cidr) ;
-                BigDecimal ipamAllocationId = getIpamAllocationIdByPrefixAndVpc(netrisV6Cidr, associatedVpc);
-                if (ipamAllocationId == null) {
-                    InlineResponse2004Data createdIpamAllocation = createIpamAllocationInternal(netrisV6IpamAllocationName, netrisV6Cidr, associatedVpc);
-                    if (Objects.isNull(createdIpamAllocation)) {
-                        throw new CloudRuntimeException(String.format("Failed to create Netris IPAM Allocation %s for VPC %s", netrisV6IpamAllocationName, netrisVpcName));
+            String netrisVnetName = NetrisResourceObjectUtils.retrieveNetrisResourceObjectName(cmd, NetrisResourceObjectUtils.NetrisObjectType.VNET, vNetName);
+
+            if (!isL2) {
+                String netrisSubnetName = NetrisResourceObjectUtils.retrieveNetrisResourceObjectName(cmd, NetrisResourceObjectUtils.NetrisObjectType.IPAM_SUBNET, String.valueOf(cmd.getVpcId()), vnetCidr);
+                createIpamSubnetInternal(netrisSubnetName, vnetCidr, SubnetBody.PurposeEnum.COMMON, associatedVpc, ipv4GlobalRouting);
+                if (Objects.nonNull(netrisV6Cidr)) {
+                    String netrisV6IpamAllocationName = NetrisResourceObjectUtils.retrieveNetrisResourceObjectName(cmd, NetrisResourceObjectUtils.NetrisObjectType.IPAM_ALLOCATION, netrisV6Cidr);
+                    String netrisV6SubnetName = NetrisResourceObjectUtils.retrieveNetrisResourceObjectName(cmd, NetrisResourceObjectUtils.NetrisObjectType.IPAM_SUBNET, String.valueOf(cmd.getVpcId()), netrisV6Cidr);
+                    BigDecimal ipamAllocationId = getIpamAllocationIdByPrefixAndVpc(netrisV6Cidr, associatedVpc);
+                    if (ipamAllocationId == null) {
+                        InlineResponse2004Data createdIpamAllocation = createIpamAllocationInternal(netrisV6IpamAllocationName, netrisV6Cidr, associatedVpc);
+                        if (Objects.isNull(createdIpamAllocation)) {
+                            throw new CloudRuntimeException(String.format("Failed to create Netris IPAM Allocation %s for VPC %s", netrisV6IpamAllocationName, associatedVpc.getName()));
+                        }
                     }
+                    createIpamSubnetInternal(netrisV6SubnetName, netrisV6Cidr, SubnetBody.PurposeEnum.COMMON, associatedVpc, ipv6GlobalRouting);
                 }
-                createIpamSubnetInternal(netrisV6SubnetName, netrisV6Cidr, SubnetBody.PurposeEnum.COMMON, associatedVpc, isGlobalRouting);
+                logger.debug("Successfully created IPAM Subnet for network {} on Netris", networkName);
             }
-            logger.debug("Successfully created IPAM Subnet {} for network {} on Netris", netrisSubnetName, networkName);
 
-            VnetResAddBody vnetResponse = createVnetInternal(associatedVpc, netrisVnetName, netrisGateway, netrisV6Cidr, vxlanId, netrisTag);
+            VnetResAddBody vnetResponse = createVnetInternal(associatedVpc, netrisVnetName, netrisGateway, isL2 ? null : netrisV6Cidr, vxlanId, netrisTag);
             if (vnetResponse == null || !vnetResponse.isIsSuccess()) {
                 String reason = vnetResponse == null ? "Empty response" : "Operation failed on Netris";
                 logger.debug("The Netris vNet creation {} failed: {}", vNetName, reason);
@@ -1217,11 +1296,12 @@ public class NetrisApiClientImpl implements NetrisApiClient {
         String vpcName = cmd.getVpcName();
         Long vpcId = cmd.getVpcId();
         boolean isVpc = cmd.isVpc();
+        boolean isL2 = cmd.isL2();
+
+        VPCListing associatedVpc = getAssociatedVPC(cmd);
 
-        String netrisVpcName = getNetrisVpcName(cmd, vpcId, vpcName);
-        VPCListing associatedVpc = getNetrisVpcResource(netrisVpcName);
         if (associatedVpc == null) {
-            logger.error("Failed to find Netris VPC with name: {}, to create the corresponding vNet for network {}", netrisVpcName, networkName);
+            logger.error("Failed to find Netris VPC with name: {}, to update the corresponding vNet for network {}", vpcName, networkName);
             return false;
         }
 
@@ -1243,9 +1323,32 @@ public class NetrisApiClientImpl implements NetrisApiClient {
             logger.debug("Netris vNet: {} update failed: {}", vNetName, reason);
             return false;
         }
+        if (isL2) {
+            String netrisNewVpcName = String.format("D%s-A%s-Z%s-N%s-%s", cmd.getDomainId(), cmd.getAccountId(), cmd.getZoneId(), networkId, networkName);
+            VpcEditResponseOK updatedVpc = updateVpcInternal(netrisNewVpcName, associatedVpc.getName(), tenantId, tenantName);
+            return validateNetrisVpcUpdateResponse(updatedVpc, associatedVpc.getName());
+        }
         return true;
     }
 
+    private VPCListing getAssociatedVPC(UpdateNetrisVnetCommand cmd) {
+        VPCListing associatedVpc;
+        boolean isL2 = cmd.isL2();
+        if (isL2) {
+            String l2VpcName = getL2VpcName(cmd, cmd.getPrevNetworkName());
+            associatedVpc = getVpcByNameAndTenant(l2VpcName);
+            if (associatedVpc == null) {
+                logger.error("Failed to find dedicated L2 VPC {} to update the corresponding vNet for L2 network {}", l2VpcName, cmd.getName());
+            }
+        } else {
+            Long vpcId = cmd.getVpcId();
+            String vpcName = cmd.getVpcName();
+            String netrisVpcName = getNetrisVpcName(cmd, vpcId, vpcName);
+            associatedVpc = getNetrisVpcResource(netrisVpcName);
+        }
+        return associatedVpc;
+    }
+
     private VnetResAddBody updateVnetInternal(VPCListing associatedVpc, String netrisVnetName, String prevVnetName) {
         logger.debug("Updating Netris vNet name from {} to {} ", netrisVnetName, prevVnetName);
         try {
@@ -1263,24 +1366,27 @@ public class NetrisApiClientImpl implements NetrisApiClient {
 
             vnetBody.setCustomAnycastMac(vnetBody.getCustomAnycastMac());
 
-            VnetEditBodyGateways gatewayV4 = new VnetEditBodyGateways();
-            gatewayV4.prefix(vnetsBody.getGateways().get(0).getPrefix());
-            gatewayV4.setDhcpEnabled(false);
-            VnetEditBodyDhcp dhcp = new VnetEditBodyDhcp();
-            dhcp.setEnd("");
-            dhcp.setStart("");
-            dhcp.setOptionSet(new VnetAddBodyDhcpOptionSet());
-            gatewayV4.setDhcp(dhcp);
             List<VnetEditBodyGateways> gatewaysList = new ArrayList<>();
-            gatewaysList.add(gatewayV4);
-
-            if (vnetsBody.getGateways().size() > 1 && Objects.nonNull(vnetsBody.getGateways().get(1))) {
-                String netrisV6Gateway = vnetsBody.getGateways().get(1).getPrefix();
-                VnetEditBodyGateways gatewayV6 = new VnetEditBodyGateways();
-                gatewayV6.prefix(netrisV6Gateway);
-                gatewayV6.setDhcpEnabled(false);
-                gatewayV6.setDhcp(dhcp);
-                gatewaysList.add(gatewayV6);
+            List<VnetsBodyGateways> existingGateways = vnetsBody.getGateways();
+            if (!CollectionUtils.isEmpty(existingGateways)) {
+                VnetEditBodyDhcp dhcp = new VnetEditBodyDhcp();
+                dhcp.setEnd("");
+                dhcp.setStart("");
+                dhcp.setOptionSet(new VnetAddBodyDhcpOptionSet());
+
+                VnetEditBodyGateways gatewayV4 = new VnetEditBodyGateways();
+                gatewayV4.prefix(vnetsBody.getGateways().get(0).getPrefix());
+                gatewayV4.setDhcpEnabled(false);
+                gatewayV4.setDhcp(dhcp);
+                gatewaysList.add(gatewayV4);
+
+                if (existingGateways.size() > 1 && Objects.nonNull(vnetsBody.getGateways().get(1))) {
+                    VnetEditBodyGateways gatewayV6 = new VnetEditBodyGateways();
+                    gatewayV6.prefix(vnetsBody.getGateways().get(1).getPrefix());
+                    gatewayV6.setDhcpEnabled(false);
+                    gatewayV6.setDhcp(dhcp);
+                    gatewaysList.add(gatewayV6);
+                }
             }
 
             vnetBody.setGateways(gatewaysList);
@@ -1333,14 +1439,25 @@ public class NetrisApiClientImpl implements NetrisApiClient {
         String networkName = cmd.getName();
         Long networkId = cmd.getId();
         boolean isVpc = cmd.isVpc();
+        boolean isL2 = cmd.isL2();
         String vnetCidr = cmd.getVNetCidr();
         String vnetV6Cidr = cmd.getvNetV6Cidr();
         try {
-            String netrisVpcName = getNetrisVpcName(cmd, vpcId, vpcName);
-            VPCListing associatedVpc = getNetrisVpcResource(netrisVpcName);
-            if (associatedVpc == null) {
-                logger.error("Failed to find Netris VPC with name: {}, to create the corresponding vNet for network {}", netrisVpcName, networkName);
-                return false;
+            VPCListing associatedVpc;
+            if (isL2) {
+                String l2VpcName = getL2VpcName(cmd, networkName);
+                associatedVpc = getVpcByNameAndTenant(l2VpcName);
+                if (associatedVpc == null) {
+                    logger.warn("Dedicated L2 VPC {} not found on Netris — vNet for network {} may have already been deleted", l2VpcName, networkName);
+                    return true;
+                }
+            } else {
+                String netrisVpcName = getNetrisVpcName(cmd, vpcId, vpcName);
+                associatedVpc = getNetrisVpcResource(netrisVpcName);
+                if (associatedVpc == null) {
+                    logger.error("Failed to find Netris VPC with name: {}, to delete the corresponding vNet for network {}", netrisVpcName, networkName);
+                    return false;
+                }
             }
 
             String vNetName;
@@ -1350,19 +1467,28 @@ public class NetrisApiClientImpl implements NetrisApiClient {
                 vNetName = String.format("N%s-%s", networkId, networkName);
             }
 
-            String netrisVnetName = NetrisResourceObjectUtils.retrieveNetrisResourceObjectName(cmd, NetrisResourceObjectUtils.NetrisObjectType.VNET, vNetName) ;
-            String netrisSubnetName = NetrisResourceObjectUtils.retrieveNetrisResourceObjectName(cmd, NetrisResourceObjectUtils.NetrisObjectType.IPAM_SUBNET, String.valueOf(cmd.getVpcId()), vnetCidr);
+            String netrisVnetName = NetrisResourceObjectUtils.retrieveNetrisResourceObjectName(cmd, NetrisResourceObjectUtils.NetrisObjectType.VNET, vNetName);
             FilterByVpc vpcFilter = new FilterByVpc();
             vpcFilter.add(associatedVpc.getId());
             deleteVnetInternal(associatedVpc, vpcFilter, netrisVnetName, vNetName);
-
             logger.debug("Successfully deleted vNet {}", vNetName);
-            deleteSubnetInternal(vpcFilter, netrisVnetName, netrisSubnetName);
-            if (Objects.nonNull(vnetV6Cidr)) {
-                String netrisV6IpamAllocationName = NetrisResourceObjectUtils.retrieveNetrisResourceObjectName(cmd, NetrisResourceObjectUtils.NetrisObjectType.IPAM_ALLOCATION, vnetV6Cidr);
-                String netrisV6SubnetName = NetrisResourceObjectUtils.retrieveNetrisResourceObjectName(cmd, NetrisResourceObjectUtils.NetrisObjectType.IPAM_SUBNET, String.valueOf(cmd.getVpcId()), vnetV6Cidr);
-                deleteSubnetInternal(vpcFilter, netrisVnetName, netrisV6SubnetName);
-                deleteVpcIpamAllocationInternal(associatedVpc, netrisV6IpamAllocationName);
+
+            if (isL2) {
+                // Delete the dedicated L2 VPC now that its only vNet is gone
+                logger.debug("Deleting dedicated L2 VPC {} on Netris", associatedVpc.getName());
+                VPCResponseObjectOK deleteResponse = deleteVpcInternal(associatedVpc);
+                if (deleteResponse == null || !deleteResponse.isIsSuccess()) {
+                    logger.warn("Failed to delete dedicated L2 VPC {} after vNet deletion — manual cleanup may be required", associatedVpc.getName());
+                }
+            } else {
+                String netrisSubnetName = NetrisResourceObjectUtils.retrieveNetrisResourceObjectName(cmd, NetrisResourceObjectUtils.NetrisObjectType.IPAM_SUBNET, String.valueOf(cmd.getVpcId()), vnetCidr);
+                deleteSubnetInternal(vpcFilter, netrisVnetName, netrisSubnetName);
+                if (Objects.nonNull(vnetV6Cidr)) {
+                    String netrisV6IpamAllocationName = NetrisResourceObjectUtils.retrieveNetrisResourceObjectName(cmd, NetrisResourceObjectUtils.NetrisObjectType.IPAM_ALLOCATION, vnetV6Cidr);
+                    String netrisV6SubnetName = NetrisResourceObjectUtils.retrieveNetrisResourceObjectName(cmd, NetrisResourceObjectUtils.NetrisObjectType.IPAM_SUBNET, String.valueOf(cmd.getVpcId()), vnetV6Cidr);
+                    deleteSubnetInternal(vpcFilter, netrisVnetName, netrisV6SubnetName);
+                    deleteVpcIpamAllocationInternal(associatedVpc, netrisV6IpamAllocationName);
+                }
             }
 
         } catch (Exception e) {
@@ -1542,6 +1668,18 @@ public class NetrisApiClientImpl implements NetrisApiClient {
                 logger.error("Could not find the Netris VPC resource with name {} and tenant ID {}", netrisVpcName, tenantId);
                 return false;
             }
+
+            NatGetBody existingRule = netrisNatRuleExists(staticNatRuleName);
+            if (existingRule != null) {
+                logger.debug("Static NAT rule '{}' already exists on Netris, skipping creation", staticNatRuleName);
+                return true;
+            }
+            // Backward compatibility: rule with legacy naming convention (no public IP post-fixed) exists - don't create a duplicate
+            String legacyName = getLegacyStaticNatRuleName(staticNatRuleName);
+            if (legacyName != null && netrisNatRuleExists(legacyName) != null) {
+                logger.debug("Legacy static NAT rule '{}' already exists on Netris, skipping creation of '{}'", legacyName, staticNatRuleName);
+                return true;
+            }
             // Create a /32 subnet for the DNAT IP
             createNatSubnet(cmd, natIP, vpcResource.getId());
             NatApi natApi = apiClient.getApiStubForMethod(NatApi.class);
@@ -1886,24 +2024,8 @@ public class NetrisApiClientImpl implements NetrisApiClient {
 
             vnetBody.setCustomAnycastMac("");
 
-            VnetAddBodyGateways gatewayV4 = new VnetAddBodyGateways();
-            gatewayV4.prefix(netrisGateway);
-            gatewayV4.setDhcpEnabled(false);
-            VnetAddBodyDhcp dhcp = new VnetAddBodyDhcp();
-            dhcp.setEnd("");
-            dhcp.setStart("");
-            dhcp.setOptionSet(new VnetAddBodyDhcpOptionSet());
-            gatewayV4.setDhcp(dhcp);
             List<VnetAddBodyGateways> gatewaysList = new ArrayList<>();
-            gatewaysList.add(gatewayV4);
-
-            if (Objects.nonNull(netrisV6Cidr)) {
-                VnetAddBodyGateways gatewayV6 = new VnetAddBodyGateways();
-                gatewayV6.prefix(NetUtils.getIpv6Gateway(netrisV6Cidr));
-                gatewayV6.setDhcpEnabled(false);
-                gatewayV6.setDhcp(dhcp);
-                gatewaysList.add(gatewayV6);
-            }
+            updateGatewayList(gatewaysList, netrisGateway, netrisV6Cidr);
 
             vnetBody.setGateways(gatewaysList);
             vnetBody.setGuestTenants(new ArrayList<>());
@@ -1948,6 +2070,32 @@ public class NetrisApiClientImpl implements NetrisApiClient {
         }
     }
 
+    private void updateGatewayList(List<VnetAddBodyGateways> gatewaysList, String netrisGateway, String netrisV6Cidr) {
+        if (netrisGateway != null) {
+            VnetAddBodyGateways gatewayV4 = new VnetAddBodyGateways();
+            gatewayV4.prefix(netrisGateway);
+            gatewayV4.setDhcpEnabled(false);
+            VnetAddBodyDhcp dhcp = new VnetAddBodyDhcp();
+            dhcp.setEnd("");
+            dhcp.setStart("");
+            dhcp.setOptionSet(new VnetAddBodyDhcpOptionSet());
+            gatewayV4.setDhcp(dhcp);
+            gatewaysList.add(gatewayV4);
+
+            if (Objects.nonNull(netrisV6Cidr)) {
+                VnetAddBodyDhcp dhcpV6 = new VnetAddBodyDhcp();
+                dhcpV6.setEnd("");
+                dhcpV6.setStart("");
+                dhcpV6.setOptionSet(new VnetAddBodyDhcpOptionSet());
+                VnetAddBodyGateways gatewayV6 = new VnetAddBodyGateways();
+                gatewayV6.prefix(NetUtils.getIpv6Gateway(netrisV6Cidr));
+                gatewayV6.setDhcpEnabled(false);
+                gatewayV6.setDhcp(dhcpV6);
+                gatewaysList.add(gatewayV6);
+            }
+        }
+    }
+
     private String getNetrisVpcNameSuffix(Long vpcId, String vpcName, Long networkId, String networkName, boolean isVpc) {
         String suffix = null;
         if (isVpc) {
@@ -1971,12 +2119,25 @@ public class NetrisApiClientImpl implements NetrisApiClient {
                 return null;
             }
             return data.get(0);
-
         } catch (ApiException e) {
             throw new CloudRuntimeException("Failed to list Netris NAT rules");
         }
     }
 
+    /**
+     * Derives the legacy static NAT rule name (without the public IP suffix) from the new name format.
+     * New format: ...-VM{vmId}-STATICNAT-{publicIp}
+     * Legacy format: ...-VM{vmId}-STATICNAT
+     * Returns null if the name doesn't match the new format.
+     */
+    private String getLegacyStaticNatRuleName(String newName) {
+        int idx = newName.indexOf("-STATICNAT-");
+        if (idx == -1) {
+            return null;
+        }
+        return newName.substring(0, idx + "-STATICNAT".length());
+    }
+
     private VPCListing getNetrisVpcResource(String netrisVpcName) {
         VPCListing vpcResource = getVpcByNameAndTenant(netrisVpcName);
         if (vpcResource == null) {
diff --git a/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/service/NetrisElement.java b/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/service/NetrisElement.java
index 0e798ebb6be..0e716cc4979 100644
--- a/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/service/NetrisElement.java
+++ b/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/service/NetrisElement.java
@@ -199,8 +199,12 @@ public class NetrisElement extends AdapterBase implements DhcpServiceProvider, D
         Map<Network.Capability, String> sourceNatCapabilities = new HashMap<>();
         sourceNatCapabilities.put(Network.Capability.RedundantRouter, "true");
         sourceNatCapabilities.put(Network.Capability.SupportedSourceNatTypes, "peraccount");
-        capabilities.put(Network.Service.Gateway, null);
         capabilities.put(Network.Service.SourceNat, sourceNatCapabilities);
+
+        final Map<Network.Capability, String> gatewayCapabilities = new HashMap<>();
+        gatewayCapabilities.put(Network.Capability.RedundantRouter, "true");
+        capabilities.put(Network.Service.Gateway, gatewayCapabilities);
+
         return capabilities;
     }
 
@@ -360,8 +364,12 @@ public class NetrisElement extends AdapterBase implements DhcpServiceProvider, D
 
     @Override
     public boolean destroy(Network network, ReservationContext context) throws ConcurrentOperationException, ResourceUnavailableException {
-        Account account = accountManager.getAccount(network.getAccountId());
         NetworkVO networkVO = networkDao.findById(network.getId());
+        if (Network.GuestType.L2.equals(networkVO.getGuestType())) {
+            logger.trace("Skipping NetrisElement.destroy() for L2 network {} - handled by guru trash()", network.getId());
+            return true;
+        }
+        Account account = accountManager.getAccount(network.getAccountId());
         DataCenterVO zone = dataCenterDao.findById(network.getDataCenterId());
         DomainVO domain = domainDao.findById(account.getDomainId());
         if (Objects.isNull(zone)) {
@@ -377,7 +385,7 @@ public class NetrisElement extends AdapterBase implements DhcpServiceProvider, D
                 vpcName = vpc.getName();
             }
         }
-        netrisService.deleteVnetResource(zone.getId(), account.getId(), domain.getId(), vpcName, vpcId, networkVO.getName(), network.getId(), network.getCidr());
+        netrisService.deleteVnetResource(zone.getId(), account.getId(), domain.getId(), vpcName, vpcId, networkVO.getName(), network.getId(), network.getCidr(), false);
         return true;
     }
 
@@ -622,7 +630,7 @@ public class NetrisElement extends AdapterBase implements DhcpServiceProvider, D
                 .setNetworkResourceName(netrisObject.getNetworkResourceName())
                 .setVpcResource(netrisObject.isVpcResource())
                 .setVmId(Objects.nonNull(vm) ? vm.getId() : 0)
-                .setVmIp(Objects.nonNull(vm) ? vm.getPrivateIpAddress() : null)
+                .setVmIp(rule.getDestinationIpAddress().addr())
                 .setPublicIp(publicIp.getAddress().addr())
                 .setPrivatePort(privatePort)
                 .setPublicPort(publicPort)
diff --git a/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/service/NetrisGuestNetworkGuru.java b/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/service/NetrisGuestNetworkGuru.java
index df566e90b67..de745626cea 100644
--- a/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/service/NetrisGuestNetworkGuru.java
+++ b/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/service/NetrisGuestNetworkGuru.java
@@ -17,6 +17,7 @@
 package org.apache.cloudstack.service;
 
 import com.cloud.dc.DataCenter;
+import com.cloud.dc.DataCenterVO;
 import com.cloud.deploy.DeployDestination;
 import com.cloud.deploy.DeploymentPlan;
 import com.cloud.domain.DomainVO;
@@ -44,6 +45,7 @@ import com.cloud.offerings.NetworkOfferingVO;
 import com.cloud.user.Account;
 import com.cloud.utils.db.DB;
 import com.cloud.utils.exception.CloudRuntimeException;
+import com.cloud.utils.net.NetUtils;
 import com.cloud.vm.NicProfile;
 import com.cloud.vm.ReservationContext;
 import com.cloud.vm.VirtualMachine;
@@ -51,6 +53,7 @@ import com.cloud.vm.VirtualMachineProfile;
 import org.apache.cloudstack.api.ApiCommandResourceType;
 import org.apache.cloudstack.api.ApiConstants;
 import org.apache.cloudstack.context.CallContext;
+import org.apache.cloudstack.resource.NetrisResourceObjectUtils;
 
 import javax.inject.Inject;
 import java.util.Objects;
@@ -75,10 +78,12 @@ public class NetrisGuestNetworkGuru  extends GuestNetworkGuru implements Network
     @Override
     public boolean canHandle(NetworkOffering offering, DataCenter.NetworkType networkType,
                              PhysicalNetwork physicalNetwork) {
+        NetworkOffering.NetworkMode networkMode = NetrisResourceObjectUtils.getNetworkMode(offering.getNetworkMode());
         return networkType == DataCenter.NetworkType.Advanced && isMyTrafficType(offering.getTrafficType())
-                && isMyIsolationMethod(physicalNetwork) && (NetworkOffering.NetworkMode.ROUTED.equals(offering.getNetworkMode())
+                && isMyIsolationMethod(physicalNetwork) && (NetworkOffering.NetworkMode.ROUTED.equals(networkMode)
                 || (networkOfferingServiceMapDao.isProviderForNetworkOffering(
-                offering.getId(), Network.Provider.Netris) && NetworkOffering.NetworkMode.NATTED.equals(offering.getNetworkMode())));
+                offering.getId(), Network.Provider.Netris) && NetworkOffering.NetworkMode.NATTED.equals(networkMode))
+                || (Network.GuestType.L2.equals(offering.getGuestType())));
     }
 
     @Override
@@ -174,8 +179,10 @@ public class NetrisGuestNetworkGuru  extends GuestNetworkGuru implements Network
                 vpcName = vpc.getName();
             }
         }
+        NetworkOfferingVO networkOfferingVO = networkOfferingDao.findById(network.getNetworkOfferingId());
+        boolean isL2 = networkOfferingVO != null && Network.GuestType.L2.equals(networkOfferingVO.getGuestType());
         return netrisService.updateVnetResource(network.getDataCenterId(), network.getAccountId(), network.getDomainId(),
-                vpcName, vpcId, network.getName(), network.getId(), prevNetworkName);
+                vpcName, vpcId, network.getName(), network.getId(), prevNetworkName, isL2);
     }
 
     @Override
@@ -201,7 +208,8 @@ public class NetrisGuestNetworkGuru  extends GuestNetworkGuru implements Network
         if (network.getName() != null) {
             implemented.setName(network.getName());
         }
-        allocateVnet(network, implemented, network.getDataCenterId(), network.getPhysicalNetworkId(), network.getReservationId());
+
+        allocateVnet(network, implemented, network.getDataCenterId(), network.getPhysicalNetworkId(), context != null ? context.getReservationId() : null);
         return implemented;
     }
 
@@ -210,12 +218,13 @@ public class NetrisGuestNetworkGuru  extends GuestNetworkGuru implements Network
             throws InsufficientVirtualNetworkCapacityException {
         String vnet = null;
         Long networkId = implemented.getId() > 0 ? implemented.getId() : network.getId();
+        long accountId = implemented.getAccountId() > 0 ? implemented.getAccountId() : network.getAccountId();
         if (network.getBroadcastUri() == null) {
             NetworkDetailVO netrisVnetDetail = networkDetailsDao.findDetail(networkId, ApiConstants.NETRIS_VXLAN_ID);
             if (nonNull(netrisVnetDetail)) {
                 vnet = netrisVnetDetail.getValue();
             } else {
-                vnet = _dcDao.allocateVnet(dcId, physicalNetworkId, network.getAccountId(), reservationId, UseSystemGuestVlans.valueIn(network.getAccountId()));
+                vnet = _dcDao.allocateVnet(dcId, physicalNetworkId, accountId, reservationId, UseSystemGuestVlans.valueIn(accountId));
                 if (vnet == null) {
                     throw new InsufficientVirtualNetworkCapacityException("Unable to allocate vnet as a " + "part of network " + network + " implement ", DataCenter.class,
                             dcId);
@@ -223,7 +232,7 @@ public class NetrisGuestNetworkGuru  extends GuestNetworkGuru implements Network
                 networkDetailsDao.addDetail(networkId, ApiConstants.NETRIS_VXLAN_ID, vnet, true);
             }
             implemented.setBroadcastUri(Networks.BroadcastDomainType.Netris.toUri(vnet));
-            ActionEventUtils.onCompletedActionEvent(CallContext.current().getCallingUserId(), network.getAccountId(), EventVO.LEVEL_INFO, EventTypes.EVENT_ZONE_VXLAN_ASSIGN,
+            ActionEventUtils.onCompletedActionEvent(CallContext.current().getCallingUserId(), accountId, EventVO.LEVEL_INFO, EventTypes.EVENT_ZONE_VXLAN_ASSIGN,
                     "Assigned Zone vNet: " + vnet + " Network Id: " + networkId, networkId, ApiCommandResourceType.Network.toString(), 0);
         } else {
             implemented.setBroadcastUri(network.getBroadcastUri());
@@ -264,8 +273,8 @@ public class NetrisGuestNetworkGuru  extends GuestNetworkGuru implements Network
         }
 
         NetworkOfferingVO networkOfferingVO = networkOfferingDao.findById(network.getNetworkOfferingId());
-
-        if (isNull(network.getVpcId()) && networkOfferingVO.getNetworkMode().equals(NetworkOffering.NetworkMode.NATTED)) {
+        NetworkOffering.NetworkMode networkMode = NetrisResourceObjectUtils.getNetworkMode(networkOfferingVO.getNetworkMode());
+        if (isNull(network.getVpcId()) && NetworkOffering.NetworkMode.NATTED.equals(networkMode)) {
             // Netris Natted mode
             long domainId = domain.getId();
             long accountId = account.getId();
@@ -299,6 +308,80 @@ public class NetrisGuestNetworkGuru  extends GuestNetworkGuru implements Network
         // Do nothing
     }
 
+    private void releaseAllocatedVnet(Network network) {
+        String vxlanId = null;
+        if (Objects.nonNull(network.getBroadcastUri())) {
+            vxlanId = Networks.BroadcastDomainType.getValue(network.getBroadcastUri());
+        } else {
+            NetworkDetailVO vxlanDetail = networkDetailsDao.findDetail(network.getId(), ApiConstants.NETRIS_VXLAN_ID);
+            if (Objects.nonNull(vxlanDetail)) {
+                vxlanId = vxlanDetail.getValue();
+            }
+        }
+
+        if (Objects.isNull(vxlanId)) {
+            logger.debug("No VXLAN allocated for Netris network {}, nothing to release", network.getId());
+            return;
+        }
+
+        logger.debug("Releasing VXLAN {} for Netris network {}", vxlanId, network.getId());
+        _dcDao.releaseVnet(vxlanId, network.getDataCenterId(), network.getPhysicalNetworkId(),
+                network.getAccountId(), network.getReservationId());
+        // Attempt to clean up with null reservation ID
+        _dcDao.releaseVnet(vxlanId, network.getDataCenterId(), network.getPhysicalNetworkId(),
+                network.getAccountId(), null);
+        if (network.getAccountId() != 0) {
+            _dcDao.releaseVnet(vxlanId, network.getDataCenterId(), network.getPhysicalNetworkId(),
+                    0L, null);
+        }
+        ActionEventUtils.onCompletedActionEvent(CallContext.current().getCallingUserId(), network.getAccountId(),
+                EventVO.LEVEL_INFO, EventTypes.EVENT_ZONE_VXLAN_RELEASE,
+                "Released Zone vNet: " + vxlanId + " for Network: " + network.getId(),
+                network.getDataCenterId(), ApiCommandResourceType.Zone.toString(), 0);
+
+        NetworkDetailVO vxlanDetail = networkDetailsDao.findDetail(network.getId(), ApiConstants.NETRIS_VXLAN_ID);
+        if (Objects.nonNull(vxlanDetail)) {
+            networkDetailsDao.removeDetail(network.getId(), ApiConstants.NETRIS_VXLAN_ID);
+        }
+    }
+
+    @Override
+    public boolean trash(Network network, NetworkOffering offering) {
+        if (Network.GuestType.L2.equals(offering.getGuestType())) {
+            NetworkVO networkVO = _networkDao.findById(network.getId());
+            if (networkVO == null) {
+                logger.warn("Could not find network {} to trash, skipping Netris L2 cleanup", network.getId());
+                return super.trash(network, offering);
+            }
+
+            DataCenterVO zone = _dcDao.findById(network.getDataCenterId());
+            if (Objects.isNull(zone)) {
+                logger.error("Cannot find zone with ID {} while trashing L2 Netris network {}", network.getDataCenterId(), network.getId());
+                return false;
+            }
+
+            Account account = accountDao.findById(network.getAccountId());
+            if (Objects.isNull(account)) {
+                logger.error("Cannot find account with ID {} while trashing L2 Netris network {}", network.getAccountId(), network.getId());
+                return false;
+            }
+
+            DomainVO domain = domainDao.findById(account.getDomainId());
+            if (Objects.isNull(domain)) {
+                logger.error("Cannot find domain with ID {} while trashing L2 Netris network {}", account.getDomainId(), network.getId());
+                return false;
+            }
+
+            logger.debug("Trashing L2 Netris network {}, deleting Netris vNet resource", network.getId());
+            netrisService.deleteVnetResource(zone.getId(), account.getId(), domain.getId(),
+                    null, null, networkVO.getName(), network.getId(), network.getCidr(), true);
+        }
+
+        releaseAllocatedVnet(network);
+
+        return super.trash(network, offering);
+    }
+
     public void createNetrisVnet(NetworkVO networkVO, DataCenter zone) {
         Account account = accountDao.findById(networkVO.getAccountId());
         if (isNull(account)) {
@@ -312,33 +395,38 @@ public class NetrisGuestNetworkGuru  extends GuestNetworkGuru implements Network
         }
         String vpcName = null;
         Long vpcId = null;
-        Boolean globalRouting = null;
+        boolean isL2 = false;
         long networkOfferingId = networkVO.getNetworkOfferingId();
+
         NetworkOfferingVO networkOfferingVO = networkOfferingDao.findById(networkOfferingId);
-        if (NetworkOffering.NetworkMode.ROUTED.equals(networkOfferingVO.getNetworkMode())) {
-            globalRouting = true;
+        NetUtils.InternetProtocol internetProtocol = networkOfferingDao.getNetworkOfferingInternetProtocol(networkOfferingVO.getId());
+        NetworkOffering.NetworkMode networkMode = networkOfferingVO.getNetworkMode();
+        if (Network.GuestType.L2.equals(networkOfferingVO.getGuestType())) {
+            isL2 = true;
         }
-        if (nonNull(networkVO.getVpcId())) {
-            VpcVO vpc = _vpcDao.findById(networkVO.getVpcId());
-            if (isNull(vpc)) {
-                throw new CloudRuntimeException(String.format("Failed to find VPC network with id: %s", networkVO.getVpcId()));
-            }
-            vpcName = vpc.getName();
-            vpcId = vpc.getId();
-        } else {
-            logger.debug(String.format("Creating IPAM Allocation before creating IPAM Subnet", networkVO.getName()));
-            boolean isSourceNatSupported = !NetworkOffering.NetworkMode.ROUTED.equals(networkOfferingVO.getNetworkMode()) &&
-                    networkOfferingServiceMapDao.areServicesSupportedByNetworkOffering(networkVO.getNetworkOfferingId(), Network.Service.SourceNat);
-            boolean result = netrisService.createVpcResource(zone.getId(), networkVO.getAccountId(), networkVO.getDomainId(),
-                    networkVO.getId(), networkVO.getName(), isSourceNatSupported, networkVO.getCidr(), false);
-            if (!result) {
-                String msg = String.format("Error creating Netris VPC for the network: %s", networkVO.getName());
-                logger.error(msg);
-                throw new CloudRuntimeException(msg);
+        if (!isL2) {
+            if (nonNull(networkVO.getVpcId())) {
+                VpcVO vpc = _vpcDao.findById(networkVO.getVpcId());
+                if (isNull(vpc)) {
+                    throw new CloudRuntimeException(String.format("Failed to find VPC network with id: %s", networkVO.getVpcId()));
+                }
+                vpcName = vpc.getName();
+                vpcId = vpc.getId();
+            } else {
+                logger.debug(String.format("Creating IPAM Allocation before creating IPAM Subnet", networkVO.getName()));
+                boolean isSourceNatSupported = !NetworkOffering.NetworkMode.ROUTED.equals(networkOfferingVO.getNetworkMode()) &&
+                        networkOfferingServiceMapDao.areServicesSupportedByNetworkOffering(networkVO.getNetworkOfferingId(), Network.Service.SourceNat);
+                boolean result = netrisService.createVpcResource(zone.getId(), networkVO.getAccountId(), networkVO.getDomainId(),
+                        networkVO.getId(), networkVO.getName(), isSourceNatSupported, networkVO.getCidr(), false);
+                if (!result) {
+                    String msg = String.format("Error creating Netris VPC for the network: %s", networkVO.getName());
+                    logger.error(msg);
+                    throw new CloudRuntimeException(msg);
+                }
             }
         }
         boolean result = netrisService.createVnetResource(zone.getId(), account.getId(), domain.getId(), vpcName, vpcId,
-                networkVO.getName(), networkVO.getId(), networkVO.getCidr(), globalRouting);
+                networkVO.getName(), networkVO.getId(), networkVO.getCidr(), networkMode, internetProtocol, isL2);
         if (!result) {
             throw new CloudRuntimeException("Failed to create Netris vNet resource for network: " + networkVO.getName());
         }
diff --git a/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/service/NetrisPublicNetworkGuru.java b/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/service/NetrisPublicNetworkGuru.java
index f1cc49b7125..7a89412fa07 100644
--- a/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/service/NetrisPublicNetworkGuru.java
+++ b/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/service/NetrisPublicNetworkGuru.java
@@ -27,7 +27,6 @@ import com.cloud.network.dao.IPAddressVO;
 import com.cloud.network.dao.NetworkVO;
 import com.cloud.network.guru.PublicNetworkGuru;
 import com.cloud.network.netris.NetrisService;
-import com.cloud.network.vpc.VpcOffering;
 import com.cloud.network.vpc.VpcOfferingVO;
 import com.cloud.network.vpc.VpcVO;
 import com.cloud.offering.NetworkOffering;
@@ -36,6 +35,7 @@ import com.cloud.utils.exception.CloudRuntimeException;
 import com.cloud.vm.NicProfile;
 import com.cloud.vm.VirtualMachineProfile;
 import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.resource.NetrisResourceObjectUtils;
 import org.apache.commons.collections.CollectionUtils;
 
 import javax.inject.Inject;
@@ -110,7 +110,8 @@ public class NetrisPublicNetworkGuru extends PublicNetworkGuru {
                 Network.Service[] services = { Network.Service.SourceNat };
                 long networkOfferingId = vpc.getVpcOfferingId();
                 VpcOfferingVO vpcVO = vpcOfferingDao.findById(networkOfferingId);
-                boolean sourceNatEnabled = !NetworkOffering.NetworkMode.ROUTED.equals(vpcVO.getNetworkMode()) &&
+                NetworkOffering.NetworkMode vpcNetworkMode = NetrisResourceObjectUtils.getNetworkMode(vpcVO.getNetworkMode());
+                boolean sourceNatEnabled = !NetworkOffering.NetworkMode.ROUTED.equals(vpcNetworkMode) &&
                         vpcOfferingServiceMapDao.areServicesSupportedByVpcOffering(vpc.getVpcOfferingId(), services);
 
                 logger.info(String.format("Creating Netris VPC %s", vpc.getName()));
@@ -121,9 +122,7 @@ public class NetrisPublicNetworkGuru extends PublicNetworkGuru {
                     throw new CloudRuntimeException(msg);
                 }
 
-                boolean hasNatSupport = false;
-                VpcOffering vpcOffering = vpcOfferingDao.findById(vpc.getVpcOfferingId());
-                hasNatSupport = NetworkOffering.NetworkMode.NATTED.equals(vpcOffering.getNetworkMode());
+                boolean hasNatSupport = NetworkOffering.NetworkMode.NATTED.equals(NetrisResourceObjectUtils.getNetworkMode(vpcVO.getNetworkMode()));
 
                 if (!hasNatSupport) {
                     return nic;
diff --git a/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/service/NetrisServiceImpl.java b/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/service/NetrisServiceImpl.java
index 6cd944c4678..5329a199d31 100644
--- a/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/service/NetrisServiceImpl.java
+++ b/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/service/NetrisServiceImpl.java
@@ -38,6 +38,7 @@ import com.cloud.network.rules.FirewallRule;
 import com.cloud.network.vpc.StaticRoute;
 import com.cloud.network.vpc.Vpc;
 import com.cloud.network.vpc.dao.VpcDao;
+import com.cloud.offering.NetworkOffering;
 import io.netris.model.NatPostBody;
 import org.apache.cloudstack.agent.api.CreateOrUpdateNetrisACLCommand;
 import com.cloud.utils.Pair;
@@ -222,14 +223,16 @@ public class NetrisServiceImpl implements NetrisService, Configurable {
     }
 
     @Override
-    public boolean createVnetResource(Long zoneId, long accountId, long domainId, String vpcName, Long vpcId, String networkName, Long networkId, String cidr, Boolean globalRouting) {
+    public boolean createVnetResource(Long zoneId, long accountId, long domainId, String vpcName, Long vpcId, String networkName, Long networkId, String cidr, NetworkOffering.NetworkMode networkMode, NetUtils.InternetProtocol internetProtocol, boolean isL2) {
         NetworkVO network = networkDao.findById(networkId);
         String vxlanId = Networks.BroadcastDomainType.getValue(network.getBroadcastUri());
         CreateNetrisVnetCommand cmd = new CreateNetrisVnetCommand(zoneId, accountId, domainId, vpcName, vpcId, networkName, networkId, cidr, network.getGateway(), !Objects.isNull(vpcId));
         cmd.setVxlanId(Integer.parseInt(vxlanId));
+        cmd.setL2(isL2);
         NetrisProviderVO netrisProvider = netrisProviderDao.findByZoneId(zoneId);
         cmd.setNetrisTag(netrisProvider.getNetrisTag());
-        cmd.setGlobalRouting(globalRouting);
+        cmd.setNetworkMode(networkMode);
+        cmd.setInternetProtocol(internetProtocol);
         if (Objects.nonNull(networkId)) {
             Ipv6GuestPrefixSubnetNetworkMapVO ipv6PrefixNetworkMapVO = ipv6PrefixNetworkMapDao.findByNetworkId(networkId);
             if (Objects.nonNull(ipv6PrefixNetworkMapVO)) {
@@ -241,18 +244,20 @@ public class NetrisServiceImpl implements NetrisService, Configurable {
     }
 
     @Override
-    public boolean updateVnetResource(Long zoneId, long accountId, long domainId, String vpcName, Long vpcId, String networkName, Long networkId, String prevNetworkName) {
+    public boolean updateVnetResource(Long zoneId, long accountId, long domainId, String vpcName, Long vpcId, String networkName, Long networkId, String prevNetworkName, boolean isL2) {
         UpdateNetrisVnetCommand cmd = new UpdateNetrisVnetCommand(zoneId, accountId, domainId, networkName, networkId, Objects.nonNull(vpcId));
         cmd.setPrevNetworkName(prevNetworkName);
         cmd.setVpcId(vpcId);
         cmd.setVpcName(vpcName);
+        cmd.setL2(isL2);
         NetrisAnswer answer = sendNetrisCommand(cmd, zoneId);
         return answer.getResult();
     }
 
     @Override
-    public boolean deleteVnetResource(long zoneId, long accountId, long domainId, String vpcName, Long vpcId, String networkName, Long networkId, String cidr) {
+    public boolean deleteVnetResource(long zoneId, long accountId, long domainId, String vpcName, Long vpcId, String networkName, Long networkId, String cidr, boolean isL2) {
         DeleteNetrisVnetCommand cmd = new DeleteNetrisVnetCommand(zoneId, accountId, domainId, networkName, networkId, vpcName, vpcId, cidr, Objects.nonNull(vpcName));
+        cmd.setL2(isL2);
         Ipv6GuestPrefixSubnetNetworkMapVO ipv6PrefixNetworkMapVO = ipv6PrefixNetworkMapDao.findByNetworkId(networkId);
         if (Objects.nonNull(ipv6PrefixNetworkMapVO)) {
             cmd.setvNetV6Cidr(ipv6PrefixNetworkMapVO.getSubnet());
@@ -354,7 +359,7 @@ public class NetrisServiceImpl implements NetrisService, Configurable {
         cmd.setNatRuleType("STATICNAT");
         cmd.setNatIp(staticNatIp);
         cmd.setVmIp(vmIp);
-        String[] suffixes = getStaticNatResourceSuffixes(vpcId, networkId, isForVpc, vmId);
+        String[] suffixes = getStaticNatResourceSuffixes(vpcId, networkId, isForVpc, vmId, staticNatIp);
         String dnatRuleName = NetrisResourceObjectUtils.retrieveNetrisResourceObjectName(cmd, NetrisResourceObjectUtils.NetrisObjectType.STATICNAT, suffixes);
         cmd.setNatRuleName(dnatRuleName);
         NetrisAnswer answer = sendNetrisCommand(cmd, zoneId);
@@ -375,7 +380,7 @@ public class NetrisServiceImpl implements NetrisService, Configurable {
             networkId = networkResourceId;
         }
         DeleteNetrisNatRuleCommand cmd = new DeleteNetrisNatRuleCommand(zoneId, accountId, domainId, vpcName, vpcId, networkName, networkId, isForVpc);
-        String suffixes[] = getStaticNatResourceSuffixes(vpcId, networkId, isForVpc, vmId);
+        String suffixes[] = getStaticNatResourceSuffixes(vpcId, networkId, isForVpc, vmId, staticNatIp);
         String dnatRuleName = NetrisResourceObjectUtils.retrieveNetrisResourceObjectName(cmd, NetrisResourceObjectUtils.NetrisObjectType.STATICNAT, suffixes);
         cmd.setNatRuleName(dnatRuleName);
         cmd.setNatRuleType("STATICNAT");
@@ -547,10 +552,9 @@ public class NetrisServiceImpl implements NetrisService, Configurable {
         return answer.getResult();
     }
 
-    public static String[] getStaticNatResourceSuffixes(Long vpcId, Long networkId, boolean isForVpc, long vmId) {
-        String[] suffixes = new String[2];
-        suffixes[0] = isForVpc ? String.valueOf(vpcId) : String.valueOf(networkId);
-        suffixes[1] = String.valueOf(vmId);
-        return suffixes;
+    public static String[] getStaticNatResourceSuffixes(Long vpcId, Long networkId, boolean isForVpc, long vmId, String publicIp) {
+        String resourceId = isForVpc ? String.valueOf(vpcId) : String.valueOf(networkId);
+        // Always include the public IP to ensure uniqueness across multiple static NATs on the same VM
+        return new String[]{resourceId, String.valueOf(vmId), publicIp};
     }
 }
diff --git a/plugins/network-elements/netris/src/test/java/org/apache/cloudstack/agent/api/CreateNetrisVnetCommandTest.java b/plugins/network-elements/netris/src/test/java/org/apache/cloudstack/agent/api/CreateNetrisVnetCommandTest.java
index cc367d181e3..66fb77d322e 100644
--- a/plugins/network-elements/netris/src/test/java/org/apache/cloudstack/agent/api/CreateNetrisVnetCommandTest.java
+++ b/plugins/network-elements/netris/src/test/java/org/apache/cloudstack/agent/api/CreateNetrisVnetCommandTest.java
@@ -16,6 +16,8 @@
 // under the License.
 package org.apache.cloudstack.agent.api;
 
+import com.cloud.offering.NetworkOffering;
+import com.cloud.utils.net.NetUtils;
 import org.junit.Assert;
 import org.junit.Test;
 
@@ -34,7 +36,8 @@ public class CreateNetrisVnetCommandTest {
     private static final Integer VXLAN_ID = 100;
     private static final String NETRIS_TAG = "test-tag";
     private static final String IPV6_CIDR = "2001:db8::/64";
-    private static final Boolean GLOBAL_ROUTING = true;
+    private static final NetworkOffering.NetworkMode NETWORK_MODE = NetworkOffering.NetworkMode.NATTED;
+    private static final NetUtils.InternetProtocol INTERNET_PROTOCOL = NetUtils.InternetProtocol.IPv4;
 
     @Test
     public void testConstructorAndGetters() {
@@ -65,13 +68,15 @@ public class CreateNetrisVnetCommandTest {
         command.setVxlanId(VXLAN_ID);
         command.setNetrisTag(NETRIS_TAG);
         command.setIpv6Cidr(IPV6_CIDR);
-        command.setGlobalRouting(GLOBAL_ROUTING);
+        command.setInternetProtocol(NetUtils.InternetProtocol.IPv4);
+        command.setNetworkMode(NetworkOffering.NetworkMode.NATTED);
 
         // Assert
         Assert.assertEquals(VXLAN_ID, command.getVxlanId());
         Assert.assertEquals(NETRIS_TAG, command.getNetrisTag());
         Assert.assertEquals(IPV6_CIDR, command.getIpv6Cidr());
-        Assert.assertEquals(GLOBAL_ROUTING, command.isGlobalRouting());
+        Assert.assertEquals(NETWORK_MODE, command.getNetworkMode());
+        Assert.assertEquals(INTERNET_PROTOCOL, command.getInternetProtocol());
     }
 
     @Test
diff --git a/plugins/network-elements/netris/src/test/java/org/apache/cloudstack/resource/NetrisResourceObjectUtilsTest.java b/plugins/network-elements/netris/src/test/java/org/apache/cloudstack/resource/NetrisResourceObjectUtilsTest.java
index 44bd9824b2d..7f0ceb596bf 100644
--- a/plugins/network-elements/netris/src/test/java/org/apache/cloudstack/resource/NetrisResourceObjectUtilsTest.java
+++ b/plugins/network-elements/netris/src/test/java/org/apache/cloudstack/resource/NetrisResourceObjectUtilsTest.java
@@ -100,10 +100,12 @@ public class NetrisResourceObjectUtilsTest {
     @Test
     public void testStaticNatName() {
         long vmId = 1234L;
+        String publicIp = "203.0.113.5";
         CreateOrUpdateNetrisNatCommand cmd = new CreateOrUpdateNetrisNatCommand(zoneId, accountId, domainId, vpcName, vpcId, null, null, true, vpcCidr);
-        String[] suffixes = NetrisServiceImpl.getStaticNatResourceSuffixes(vpcId, null, true, vmId);
+        String[] suffixes = NetrisServiceImpl.getStaticNatResourceSuffixes(vpcId, null, true, vmId, publicIp);
         String staticNatRuleName = NetrisResourceObjectUtils.retrieveNetrisResourceObjectName(cmd, NetrisResourceObjectUtils.NetrisObjectType.STATICNAT, suffixes);
-        String expectedName = String.format("D%s-A%s-Z%s-V%s-VM%s-STATICNAT", domainId, accountId, zoneId, vpcId, vmId);
+        // Public IP always appended for uniqueness; legacy rules without IP are handled via fallback on delete
+        String expectedName = String.format("D%s-A%s-Z%s-V%s-VM%s-STATICNAT-%s", domainId, accountId, zoneId, vpcId, vmId, publicIp);
         Assert.assertEquals(expectedName, staticNatRuleName);
     }
 }
diff --git a/plugins/network-elements/netris/src/test/java/org/apache/cloudstack/service/NetrisElementTest.java b/plugins/network-elements/netris/src/test/java/org/apache/cloudstack/service/NetrisElementTest.java
index a2622549deb..1010cd48114 100644
--- a/plugins/network-elements/netris/src/test/java/org/apache/cloudstack/service/NetrisElementTest.java
+++ b/plugins/network-elements/netris/src/test/java/org/apache/cloudstack/service/NetrisElementTest.java
@@ -136,6 +136,6 @@ public class NetrisElementTest {
         ReservationContext context = Mockito.mock(ReservationContext.class);
         netrisElement.destroy(network, context);
         Mockito.verify(netrisService).deleteVnetResource(zoneId, accountId, domainId, vpcName, vpcId,
-                networkName, networkId, networkCidr);
+                networkName, networkId, networkCidr, false);
     }
 }
diff --git a/plugins/network-elements/netris/src/test/java/org/apache/cloudstack/service/NetrisGuestNetworkGuruTest.java b/plugins/network-elements/netris/src/test/java/org/apache/cloudstack/service/NetrisGuestNetworkGuruTest.java
index ad8d46c0dd8..d228ca9217c 100644
--- a/plugins/network-elements/netris/src/test/java/org/apache/cloudstack/service/NetrisGuestNetworkGuruTest.java
+++ b/plugins/network-elements/netris/src/test/java/org/apache/cloudstack/service/NetrisGuestNetworkGuruTest.java
@@ -41,6 +41,7 @@ import com.cloud.offerings.dao.NetworkOfferingDao;
 import com.cloud.offerings.dao.NetworkOfferingServiceMapDao;
 import com.cloud.user.AccountVO;
 import com.cloud.user.dao.AccountDao;
+import com.cloud.utils.net.NetUtils;
 import com.cloud.vm.ReservationContext;
 import org.junit.After;
 import org.junit.Assert;
@@ -206,12 +207,49 @@ public class NetrisGuestNetworkGuruTest {
     @Test
     public void testCreateNetrisVnetVpcNetworkRoutedMode() {
         Mockito.when(networkOffering.getNetworkMode()).thenReturn(NetworkOffering.NetworkMode.ROUTED);
+        Mockito.when(networkOfferingDao.getNetworkOfferingInternetProtocol(networkOfferingId)).thenReturn(NetUtils.InternetProtocol.IPv4);
         Mockito.when(netrisService.createVnetResource(Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(),
                 Mockito.anyString(), Mockito.anyLong(), Mockito.anyString(), Mockito.anyLong(), Mockito.anyString(),
-                Mockito.anyBoolean())).thenReturn(true);
+                Mockito.any(), Mockito.any(), Mockito.anyBoolean())).thenReturn(true);
         guru.createNetrisVnet(network, zone);
         Mockito.verify(netrisService).createVnetResource(zoneId, accountId, domainId, vpcName, vpcId,
-                networkName, networkId, networkCidr, true);
+                networkName, networkId, networkCidr, NetworkOffering.NetworkMode.ROUTED, NetUtils.InternetProtocol.IPv4, false);
+    }
+
+    @Test
+    public void testCreateNetrisVnetVpcNetworkNattedMode() {
+        Mockito.when(networkOffering.getNetworkMode()).thenReturn(NetworkOffering.NetworkMode.NATTED);
+        Mockito.when(networkOfferingDao.getNetworkOfferingInternetProtocol(networkOfferingId)).thenReturn(NetUtils.InternetProtocol.IPv4);
+        Mockito.when(netrisService.createVnetResource(Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(),
+                Mockito.anyString(), Mockito.anyLong(), Mockito.anyString(), Mockito.anyLong(), Mockito.anyString(),
+                Mockito.any(), Mockito.any(), Mockito.anyBoolean())).thenReturn(true);
+        guru.createNetrisVnet(network, zone);
+        Mockito.verify(netrisService).createVnetResource(zoneId, accountId, domainId, vpcName, vpcId,
+                networkName, networkId, networkCidr, NetworkOffering.NetworkMode.NATTED, NetUtils.InternetProtocol.IPv4, false);
+    }
+
+    @Test
+    public void testCreateNetrisVnetVpcNetworkDualStackNattedMode() {
+        Mockito.when(networkOffering.getNetworkMode()).thenReturn(NetworkOffering.NetworkMode.NATTED);
+        Mockito.when(networkOfferingDao.getNetworkOfferingInternetProtocol(networkOfferingId)).thenReturn(NetUtils.InternetProtocol.DualStack);
+        Mockito.when(netrisService.createVnetResource(Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(),
+                Mockito.anyString(), Mockito.anyLong(), Mockito.anyString(), Mockito.anyLong(), Mockito.anyString(),
+                Mockito.any(), Mockito.any(), Mockito.anyBoolean())).thenReturn(true);
+        guru.createNetrisVnet(network, zone);
+        Mockito.verify(netrisService).createVnetResource(zoneId, accountId, domainId, vpcName, vpcId,
+                networkName, networkId, networkCidr, NetworkOffering.NetworkMode.NATTED, NetUtils.InternetProtocol.DualStack, false);
+    }
+
+    @Test
+    public void testCreateNetrisVnetVpcNetworkDualStackRoutedMode() {
+        Mockito.when(networkOffering.getNetworkMode()).thenReturn(NetworkOffering.NetworkMode.ROUTED);
+        Mockito.when(networkOfferingDao.getNetworkOfferingInternetProtocol(networkOfferingId)).thenReturn(NetUtils.InternetProtocol.DualStack);
+        Mockito.when(netrisService.createVnetResource(Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(),
+                Mockito.anyString(), Mockito.anyLong(), Mockito.anyString(), Mockito.anyLong(), Mockito.anyString(),
+                Mockito.any(), Mockito.any(), Mockito.anyBoolean())).thenReturn(true);
+        guru.createNetrisVnet(network, zone);
+        Mockito.verify(netrisService).createVnetResource(zoneId, accountId, domainId, vpcName, vpcId,
+                networkName, networkId, networkCidr, NetworkOffering.NetworkMode.ROUTED, NetUtils.InternetProtocol.DualStack, false);
     }
 
     @Test
diff --git a/server/src/main/java/com/cloud/network/guru/GuestNetworkGuru.java b/server/src/main/java/com/cloud/network/guru/GuestNetworkGuru.java
index 3c677cb19e2..76a8052bff9 100644
--- a/server/src/main/java/com/cloud/network/guru/GuestNetworkGuru.java
+++ b/server/src/main/java/com/cloud/network/guru/GuestNetworkGuru.java
@@ -17,7 +17,6 @@
 package com.cloud.network.guru;
 
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.List;
 import java.util.Random;
 
@@ -529,16 +528,15 @@ public abstract class GuestNetworkGuru extends AdapterBase implements NetworkGur
             return; // Nothing to do here if the uri is null already
         }
 
-        if ((profile.getBroadcastDomainType() == BroadcastDomainType.Vlan || profile.getBroadcastDomainType() == BroadcastDomainType.Vxlan || profile.getBroadcastDomainType() == BroadcastDomainType.Netris) && !offering.isSpecifyVlan()) {
-            logger.debug("Releasing vnet for the network: {}", profile);
-            _dcDao.releaseVnet(BroadcastDomainType.getValue(profile.getBroadcastUri()), profile.getDataCenterId(), profile.getPhysicalNetworkId(), profile.getAccountId(),
+        if ((profile.getBroadcastDomainType() == BroadcastDomainType.Vlan || profile.getBroadcastDomainType() == BroadcastDomainType.Vxlan) && !offering.isSpecifyVlan()) {
+            logger.debug("Releasing vnet for the network id=" + profile.getId());
+            final String vnetValue = BroadcastDomainType.getValue(profile.getBroadcastUri());
+            _dcDao.releaseVnet(vnetValue, profile.getDataCenterId(), profile.getPhysicalNetworkId(), profile.getAccountId(),
                     profile.getReservationId());
-            String eventType = EventTypes.EVENT_ZONE_VLAN_RELEASE;
-            if (Arrays.asList(BroadcastDomainType.Vxlan, BroadcastDomainType.Netris).contains(profile.getBroadcastDomainType())) {
-                eventType = EventTypes.EVENT_ZONE_VXLAN_RELEASE;
-            }
+            String eventType = profile.getBroadcastDomainType() == BroadcastDomainType.Vxlan
+                    ? EventTypes.EVENT_ZONE_VXLAN_RELEASE : EventTypes.EVENT_ZONE_VLAN_RELEASE;
             ActionEventUtils.onCompletedActionEvent(CallContext.current().getCallingUserId(), profile.getAccountId(), EventVO.LEVEL_INFO, eventType,
-                    "Released Zone Vnet: " + BroadcastDomainType.getValue(profile.getBroadcastUri()) + " for Network: " + profile.getId(),
+                    "Released Zone Vnet: " + vnetValue + " for Network: " + profile.getId(),
                     profile.getDataCenterId(), ApiCommandResourceType.Zone.toString(), 0);
         }
 
diff --git a/server/src/main/java/com/cloud/network/vpc/VpcManagerImpl.java b/server/src/main/java/com/cloud/network/vpc/VpcManagerImpl.java
index 5717f8745ac..a4420fd270f 100644
--- a/server/src/main/java/com/cloud/network/vpc/VpcManagerImpl.java
+++ b/server/src/main/java/com/cloud/network/vpc/VpcManagerImpl.java
@@ -714,10 +714,11 @@ public class VpcManagerImpl extends ManagerBase implements VpcManager, VpcProvis
         Service redundantRouterService = Service.SourceNat;
         if (CollectionUtils.isNotEmpty(sourceNatServiceProviders)) {
             svcProviderMap.put(Service.Gateway, sourceNatServiceProviders);
-        } else if (NetworkOffering.NetworkMode.ROUTED.equals(networkMode) && org.apache.commons.lang3.StringUtils.isBlank(externalProvider)) {
-            // For Routed mode, add the Gateway service except for external providers such as NSX, Netris to not override the svcProviderMap mapping
-            svcProviderMap.put(Service.Gateway, Sets.newHashSet(Provider.VPCVirtualRouter));
+        } else if (NetworkOffering.NetworkMode.ROUTED.equals(networkMode)) {
             redundantRouterService = Service.Gateway;
+            if (org.apache.commons.lang3.StringUtils.isBlank(externalProvider)) {
+                svcProviderMap.put(Service.Gateway, Sets.newHashSet(Provider.VPCVirtualRouter));
+            }
         }
 
         validateConnectivtyServiceCapabilities(svcProviderMap.get(Service.Connectivity), serviceCapabilityList);
diff --git a/server/src/test/java/org/apache/cloudstack/service/NetrisServiceMockTest.java b/server/src/test/java/org/apache/cloudstack/service/NetrisServiceMockTest.java
index 9f590dc1807..d72dafeaabc 100644
--- a/server/src/test/java/org/apache/cloudstack/service/NetrisServiceMockTest.java
+++ b/server/src/test/java/org/apache/cloudstack/service/NetrisServiceMockTest.java
@@ -23,6 +23,8 @@ import com.cloud.network.netris.NetrisNetworkRule;
 import com.cloud.network.netris.NetrisService;
 import com.cloud.network.vpc.StaticRoute;
 import com.cloud.network.vpc.Vpc;
+import com.cloud.offering.NetworkOffering;
+import com.cloud.utils.net.NetUtils;
 
 import java.util.List;
 
@@ -48,17 +50,17 @@ public class NetrisServiceMockTest implements NetrisService {
     }
 
     @Override
-    public boolean createVnetResource(Long zoneId, long accountId, long domainId, String vpcName, Long vpcId, String networkName, Long networkId, String cidr, Boolean globalRouting) {
+    public boolean createVnetResource(Long zoneId, long accountId, long domainId, String vpcName, Long vpcId, String networkName, Long networkId, String cidr, NetworkOffering.NetworkMode networkMode, NetUtils.InternetProtocol internetProtocol, boolean isL2) {
         return true;
     }
 
     @Override
-    public boolean updateVnetResource(Long zoneId, long accountId, long domainId, String vpcName, Long vpcId, String networkName, Long networkId, String prevNetworkName) {
+    public boolean updateVnetResource(Long zoneId, long accountId, long domainId, String vpcName, Long vpcId, String networkName, Long networkId, String prevNetworkName, boolean isL2) {
         return true;
     }
 
     @Override
-    public boolean deleteVnetResource(long zoneId, long accountId, long domainId, String vpcName, Long vpcId, String networkName, Long networkId, String cidr) {
+    public boolean deleteVnetResource(long zoneId, long accountId, long domainId, String vpcName, Long vpcId, String networkName, Long networkId, String cidr, boolean isL2) {
         return true;
     }
 
diff --git a/ui/src/views/offering/AddNetworkOffering.vue b/ui/src/views/offering/AddNetworkOffering.vue
index 2b3275a11ed..cab886a761a 100644
--- a/ui/src/views/offering/AddNetworkOffering.vue
+++ b/ui/src/views/offering/AddNetworkOffering.vue
@@ -60,7 +60,7 @@
             <a-radio-button value="isolated">
               {{ $t('label.isolated') }}
             </a-radio-button>
-            <a-radio-button value="l2" v-if="form.provider !== 'NSX' && form.provider !== 'Netris'">
+            <a-radio-button value="l2" v-if="form.provider !== 'NSX'">
               {{ $t('label.l2') }}
             </a-radio-button>
             <a-radio-button value="shared" v-if="form.provider !== 'NSX' && form.provider !== 'Netris'">
@@ -311,7 +311,7 @@
             </a-list>
           </div>
         </a-form-item>
-        <a-form-item name="lbtype" ref="lbtype" :label="$t('label.lbtype')" v-if="forVpc && lbServiceChecked">
+        <a-form-item name="lbtype" ref="lbtype" :label="$t('label.lbtype')" v-if="forVpc && lbServiceChecked && form.provider !== 'Netris'">
           <a-radio-group
             v-model:value="form.lbtype"
             buttonStyle="solid"
@@ -358,7 +358,7 @@
           v-if="(guestType === 'shared' || guestType === 'isolated') && sourceNatServiceChecked && !isVpcVirtualRouterForAtLeastOneService">
           <a-switch v-model:checked="form.redundantroutercapability" />
         </a-form-item>
-        <a-form-item name="sourcenattype" ref="sourcenattype" :label="$t('label.sourcenattype')" v-if="(guestType === 'shared' || guestType === 'isolated') && sourceNatServiceChecked">
+        <a-form-item name="sourcenattype" ref="sourcenattype" :label="$t('label.sourcenattype')" v-if="(guestType === 'shared' || guestType === 'isolated') && sourceNatServiceChecked && form.provider !== 'Netris'">
           <a-radio-group
             v-model:value="form.sourcenattype"
             buttonStyle="solid">
@@ -374,8 +374,8 @@
           name="vmautoscalingcapability"
           ref="vmautoscalingcapability"
           :label="$t('label.supportsvmautoscaling')"
-          v-if="lbServiceChecked && ['Netscaler', 'VirtualRouter', 'VpcVirtualRouter'].includes(lbServiceProvider)">
-          <a-switch v-model:checked="form.vmautoscalingcapability" />
+          v-if="lbServiceChecked && ['Netscaler', 'VirtualRouter', 'VpcVirtualRouter', 'Netris'].includes(lbServiceProvider)">
+          <a-switch v-model:checked="form.vmautoscalingcapability" :disabled="lbServiceProvider === 'Netris'" />
         </a-form-item>
         <a-form-item
           name="elasticlb"
@@ -865,6 +865,12 @@ export default {
           this.supportedServices[i].provider = providers
           this.supportedServices[i].description = serviceDisplayName
         }
+        this.supportedSvcs = this.supportedServices.map(service => {
+          return {
+            ...service,
+            provider: (service.provider || []).map(provider => ({ ...provider }))
+          }
+        })
       }).finally(() => {
         this.supportedServiceLoading = false
         this.updateSupportedServices()
@@ -902,7 +908,12 @@ export default {
     },
     updateSupportedServices () {
       this.supportedServiceLoading = true
-      var supportedServices = this.supportedServices
+      var supportedServices = (this.supportedSvcs.length > 0 ? this.supportedSvcs : this.supportedServices).map(service => {
+        return {
+          ...service,
+          provider: (service.provider || []).map(provider => ({ ...provider }))
+        }
+      })
       var self = this
       if (this.provider !== 'NSX' && this.provider !== 'Netris') {
         if (this.networkmode === 'ROUTED' && this.guestType === 'isolated') {
@@ -930,12 +941,10 @@ export default {
           }
         })
         setTimeout(() => {
-          self.supportedSvcs = self.supportedServices
           self.supportedServices = supportedServices
           self.supportedServiceLoading = false
         }, 50)
       } else {
-        supportedServices = this.supportedSvcs
         supportedServices = supportedServices.filter(svc => {
           if (this.provider === 'NSX') {
             return Object.keys(this.nsxSupportedServicesMap).includes(svc.name)
@@ -959,88 +968,93 @@ export default {
           }
           return svc
         })
-        self.supportedSvcs = self.supportedServices
+
+        const externalSelectedProviders = {}
+        supportedServices.forEach(svc => {
+          const providerName = svc.provider?.[0]?.name
+          if (providerName) {
+            externalSelectedProviders[svc.name] = providerName
+          }
+        })
+        this.selectedServiceProviderMap = externalSelectedProviders
+        this.sourceNatServiceChecked = 'SourceNat' in externalSelectedProviders
+        this.lbServiceChecked = 'Lb' in externalSelectedProviders
+        this.lbServiceProvider = this.lbServiceChecked ? externalSelectedProviders.Lb : ''
+        if (this.lbServiceProvider === 'Netris') {
+          this.form.vmautoscalingcapability = true
+        }
+        this.staticNatServiceChecked = 'StaticNat' in externalSelectedProviders
+        this.staticNatServiceProvider = this.staticNatServiceChecked ? externalSelectedProviders.StaticNat : ''
+        this.connectivityServiceChecked = 'Connectivity' in externalSelectedProviders
+        this.firewallServiceChecked = 'Firewall' in externalSelectedProviders
+        this.firewallServiceProvider = this.firewallServiceChecked ? externalSelectedProviders.Firewall : ''
+        const selectedProviders = Object.values(externalSelectedProviders)
+        this.isVirtualRouterForAtLeastOneService = selectedProviders.includes('VirtualRouter')
+        this.isVpcVirtualRouterForAtLeastOneService = selectedProviders.includes('VpcVirtualRouter')
+        if ((this.isVirtualRouterForAtLeastOneService || this.isVpcVirtualRouterForAtLeastOneService) &&
+          this.serviceOfferings.length === 0) {
+          this.fetchServiceOfferingData()
+        }
+
         self.supportedServices = supportedServices
         self.supportedServiceLoading = false
       }
     },
+    refreshExternalSupportedServicesMap () {
+      const selectedProvider = this.form.provider || this.provider
+      if (selectedProvider !== 'NSX' && selectedProvider !== 'Netris') {
+        return
+      }
+
+      const selectedNetworkMode = this.form.networkmode || this.networkmode
+      const effectiveNetworkMode = selectedProvider === 'Netris' && !selectedNetworkMode ? 'NATTED' : selectedNetworkMode
+      const externalProvider = selectedProvider === 'NSX' ? this.NSX : this.Netris
+      const isNsxProvider = selectedProvider === 'NSX'
+      const commonServices = {
+        Dhcp: this.forVpc ? this.VPCVR : this.VR,
+        Dns: this.forVpc ? this.VPCVR : this.VR,
+        UserData: this.forVpc ? this.VPCVR : this.VR,
+        ...(this.forVpc && { NetworkACL: externalProvider }),
+        ...(!this.forVpc && { Firewall: externalProvider })
+      }
+
+      const nattedServices = {
+        SourceNat: externalProvider,
+        StaticNat: externalProvider,
+        PortForwarding: externalProvider,
+        Vpn: this.forVpc ? this.VPCVR : this.VR,
+        ...((!isNsxProvider || this.form.nsxsupportlb) && { Lb: externalProvider })
+      }
+
+      const serviceMap = {
+        ...commonServices,
+        ...(effectiveNetworkMode === 'NATTED' && nattedServices)
+      }
+
+      if (isNsxProvider) {
+        this.nsxSupportedServicesMap = serviceMap
+      } else {
+        this.netrisSupportedServicesMap = serviceMap
+      }
+    },
     handleForVpcChange (forVpc) {
       this.forVpc = forVpc
-      if (this.provider === 'NSX') {
-        this.nsxSupportedServicesMap = {
-          Dhcp: this.forVpc ? this.VPCVR : this.VR,
-          Dns: this.forVpc ? this.VPCVR : this.VR,
-          UserData: this.forVpc ? this.VPCVR : this.VR,
-          ...(this.networkmode === 'NATTED' && {
-            SourceNat: this.NSX,
-            StaticNat: this.NSX,
-            PortForwarding: this.NSX,
-            Lb: this.NSX
-          }),
-          ...(forVpc && { NetworkACL: this.NSX }),
-          ...(!forVpc && { Firewall: this.NSX })
-        }
-      } else if (this.provider === 'Netris') {
-        this.netrisSupportedServicesMap = {
-          Dhcp: this.forVpc ? this.VPCVR : this.VR,
-          Dns: this.forVpc ? this.VPCVR : this.VR,
-          UserData: this.forVpc ? this.VPCVR : this.VR,
-          ...(this.networkmode === 'NATTED' && {
-            SourceNat: this.Netris,
-            StaticNat: this.Netris,
-            PortForwarding: this.Netris,
-            Lb: this.Netris
-          }),
-          ...(forVpc && { NetworkACL: this.Netris }),
-          ...(!forVpc && { Firewall: this.Netris })
-        }
-      }
+      this.refreshExternalSupportedServicesMap()
       this.updateSupportedServices()
     },
     handleProviderChange (provider) {
       this.provider = provider
-      if (this.provider === 'NSX') {
-        this.nsxSupportedServicesMap = {
-          Dhcp: this.forVpc ? this.VPCVR : this.VR,
-          Dns: this.forVpc ? this.VPCVR : this.VR,
-          UserData: this.forVpc ? this.VPCVR : this.VR,
-          ...(this.networkmode === 'NATTED' && {
-            SourceNat: this.NSX,
-            StaticNat: this.NSX,
-            PortForwarding: this.NSX,
-            Lb: this.NSX
-          }),
-          ...(this.forVpc && { NetworkACL: this.NSX }),
-          ...(!this.forVpc && { Firewall: this.NSX })
-        }
-      } else if (this.provider === 'Netris') {
-        this.netrisSupportedServicesMap = {
-          Dhcp: this.forVpc ? this.VPCVR : this.VR,
-          Dns: this.forVpc ? this.VPCVR : this.VR,
-          UserData: this.forVpc ? this.VPCVR : this.VR,
-          ...(this.networkmode === 'NATTED' && {
-            SourceNat: this.Netris,
-            StaticNat: this.Netris,
-            PortForwarding: this.Netris,
-            Lb: this.Netris
-          }),
-          ...(this.forVpc && { NetworkACL: this.Netris }),
-          ...(!this.forVpc && { Firewall: this.Netris })
-        }
-      }
+      this.refreshExternalSupportedServicesMap()
       this.fetchSupportedServiceData()
     },
     handleForNetworkModeChange (networkMode) {
       this.networkmode = networkMode
+      this.refreshExternalSupportedServicesMap()
       this.fetchSupportedServiceData()
     },
     handleNsxLbService (supportLb) {
-      if (!supportLb && 'Lb' in this.nsxSupportedServicesMap) {
-        delete this.nsxSupportedServicesMap.Lb
-      }
-      if (supportLb && !('Lb' in this.nsxSupportedServicesMap)) {
-        this.nsxSupportedServicesMap.Lb = this.NSX
-      }
+      this.form.nsxsupportlb = supportLb
+      this.refreshExternalSupportedServicesMap()
       this.fetchSupportedServiceData()
     },
     handleLbTypeChange (lbType) {
@@ -1055,6 +1069,10 @@ export default {
           this.fetchRegisteredServicePackageData()
           if (provider != null & provider !== undefined) {
             this.lbServiceProvider = provider
+            this.lbServiceChecked = true
+            if (provider === 'Netris') {
+              this.form.vmautoscalingcapability = true
+            }
           }
         } else {
           this.lbServiceProvider = ''
@@ -1062,7 +1080,7 @@ export default {
         this.lbServiceChecked = checked
       } else if (service === 'StaticNat') {
         this.staticNatServiceChecked = checked
-        if (checked && provider != null & provider !== undefined) {
+        if (checked && provider != null && provider !== undefined) {
           this.staticNatServiceProvider = provider
         } else {
           this.staticNatServiceProvider = ''
@@ -1071,13 +1089,13 @@ export default {
         this.connectivityServiceChecked = checked
       } else if (service === 'Firewall') {
         this.firewallServiceChecked = checked
-        if (checked && provider != null & provider !== undefined) {
-          this.staticNatServiceProvider = provider
+        if (checked && provider != null && provider !== undefined) {
+          this.firewallServiceProvider = provider
         } else {
-          this.staticNatServiceProvider = ''
+          this.firewallServiceProvider = ''
         }
       }
-      if (checked && provider != null & provider !== undefined) {
+      if (checked && provider != null && provider !== undefined) {
         this.selectedServiceProviderMap[service] = provider
       } else {
         delete this.selectedServiceProviderMap[service]
@@ -1196,16 +1214,19 @@ export default {
             delete params.supportspublicaccess
           }
           if (supportedServices.includes('SourceNat')) {
+            const isNetrisSourceNat = this.selectedServiceProviderMap.SourceNat === 'Netris'
             if (values.redundantroutercapability === true) {
               params['serviceCapabilityList[' + serviceCapabilityIndex + '].service'] = 'SourceNat'
               params['serviceCapabilityList[' + serviceCapabilityIndex + '].capabilitytype'] = 'RedundantRouter'
               params['serviceCapabilityList[' + serviceCapabilityIndex + '].capabilityvalue'] = true
               serviceCapabilityIndex++
             }
-            params['servicecapabilitylist[' + serviceCapabilityIndex + '].service'] = 'SourceNat'
-            params['servicecapabilitylist[' + serviceCapabilityIndex + '].capabilitytype'] = 'SupportedSourceNatTypes'
-            params['servicecapabilitylist[' + serviceCapabilityIndex + '].capabilityvalue'] = values.sourcenattype
-            serviceCapabilityIndex++
+            if (!isNetrisSourceNat) {
+              params['servicecapabilitylist[' + serviceCapabilityIndex + '].service'] = 'SourceNat'
+              params['servicecapabilitylist[' + serviceCapabilityIndex + '].capabilitytype'] = 'SupportedSourceNatTypes'
+              params['servicecapabilitylist[' + serviceCapabilityIndex + '].capabilityvalue'] = values.sourcenattype
+              serviceCapabilityIndex++
+            }
             delete params.redundantroutercapability
             delete params.sourcenattype
           } else if (values.redundantroutercapability === true) {
@@ -1233,7 +1254,7 @@ export default {
             if ('vmautoscalingcapability' in values) {
               params['servicecapabilitylist[' + serviceCapabilityIndex + '].service'] = 'lb'
               params['servicecapabilitylist[' + serviceCapabilityIndex + '].capabilitytype'] = 'VmAutoScaling'
-              params['servicecapabilitylist[' + serviceCapabilityIndex + '].capabilityvalue'] = values.vmautoscalingcapability
+              params['servicecapabilitylist[' + serviceCapabilityIndex + '].capabilityvalue'] = this.selectedServiceProviderMap.Lb === 'Netris' ? true : values.vmautoscalingcapability
               serviceCapabilityIndex++
             }
             if (values.elasticlb === true) {
diff --git a/ui/src/views/offering/AddVpcOffering.vue b/ui/src/views/offering/AddVpcOffering.vue
index d909cbdc3dc..3515ff9e9dd 100644
--- a/ui/src/views/offering/AddVpcOffering.vue
+++ b/ui/src/views/offering/AddVpcOffering.vue
@@ -166,7 +166,7 @@
         <a-form-item name="distributedrouter" ref="distributedrouter" :label="$t('label.service.connectivity.distributedroutercapabilitycheckbox')" v-if="connectivityServiceChecked">
           <a-switch v-model:checked="form.distributedrouter" />
         </a-form-item>
-        <a-form-item name="redundantrouter" ref="redundantrouter" :label="$t('label.redundantrouter')" v-if="sourceNatServiceChecked">
+        <a-form-item name="redundantrouter" ref="redundantrouter" :label="$t('label.redundantrouter')" v-if="sourceNatServiceChecked || gatewayServiceChecked">
           <a-switch v-model:checked="form.redundantrouter" />
         </a-form-item>
         <a-form-item name="serviceofferingid" ref="serviceofferingid">
@@ -297,6 +297,7 @@ export default {
       isVpcVirtualRouterForAtLeastOneService: false,
       connectivityServiceChecked: false,
       sourceNatServiceChecked: false,
+      gatewayServiceChecked: false,
       selectedServiceProviderMap: {},
       ipv6NetworkOfferingEnabled: false,
       routedNetworkEnabled: false,
@@ -423,6 +424,8 @@ export default {
     },
     fetchSupportedServiceData () {
       var services = []
+      this.sourceNatServiceChecked = false
+      this.gatewayServiceChecked = false
       if (this.provider === 'NSX') {
         services.push({
           name: 'Dhcp',
@@ -509,6 +512,11 @@ export default {
           enabled: true,
           provider: [{ name: 'VpcVirtualRouter' }]
         })
+        services.push({
+          name: 'Vpn',
+          enabled: true,
+          provider: [{ name: 'VpcVirtualRouter' }]
+        })
       } else {
         services.push({
           name: 'Dhcp',
@@ -600,6 +608,13 @@ export default {
           })
         }
       }
+
+      if (this.Netris.name === this.form.provider && services.filter(svc => svc.name === 'SourceNat' && svc.provider[0].name === this.Netris.name).length > 0) {
+        this.sourceNatServiceChecked = true
+      }
+      if (this.Netris.name === this.form.provider && services.filter(svc => svc.name === 'Gateway').length > 0) {
+        this.gatewayServiceChecked = true
+      }
       for (var i in services) {
         services[i].description = services[i].name
       }
@@ -615,6 +630,10 @@ export default {
         this.form.nsxsupportlb = true
         this.handleNsxLbService(true)
       }
+      if (this.provider === 'Netris') {
+        this.form.networkmode = 'NATTED'
+        this.networkmode = 'NATTED'
+      }
       this.fetchSupportedServiceData()
     },
     handleNsxLbService (supportLb) {
@@ -753,7 +772,10 @@ export default {
             params['serviceCapabilityList[' + serviceCapabilityIndex + '].capabilityvalue'] = true
             serviceCapabilityIndex++
           } else if (values.redundantrouter === true) {
-            params['serviceCapabilityList[' + serviceCapabilityIndex + '].service'] = 'Gateway'
+            // For Netris NATTED (where SourceNat is auto-added server-side but not in the local map),
+            // use SourceNat. For all other cases (e.g. ROUTED VPC VR), use Gateway.
+            const redundantRouterService = (forNetris && values.networkmode !== 'ROUTED') ? 'SourceNat' : 'Gateway'
+            params['serviceCapabilityList[' + serviceCapabilityIndex + '].service'] = redundantRouterService
             params['serviceCapabilityList[' + serviceCapabilityIndex + '].capabilitytype'] = 'RedundantRouter'
             params['serviceCapabilityList[' + serviceCapabilityIndex + '].capabilityvalue'] = true
             serviceCapabilityIndex++


(cloudstack) 06/08: Fix issue with adding external node to a CKS cluster with Netris offering

Posted by pe...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

Pearl1594 pushed a commit to branch netris-improvements
in repository https://gitbox.apache.org/repos/asf/cloudstack.git

commit 98ae1c4899d79be61055661db1596c7626ae1666
Author: Pearl Dsilva <pe...@gmail.com>
AuthorDate: Wed Jul 8 12:45:42 2026 -0400

    Fix issue with adding external node to a CKS cluster with Netris offering
    
    Co-authored-by: Pearl Dsilva <pe...@gmail.com>
---
 .../cluster/actionworkers/KubernetesClusterActionWorker.java  |  3 ++-
 .../org/apache/cloudstack/service/NetrisApiClientImpl.java    | 11 ++++++-----
 server/src/main/java/com/cloud/network/NetworkModelImpl.java  |  6 ++++--
 3 files changed, 12 insertions(+), 8 deletions(-)

diff --git a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterActionWorker.java b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterActionWorker.java
index 6f7579930d7..7a06b11e3bf 100644
--- a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterActionWorker.java
+++ b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterActionWorker.java
@@ -484,7 +484,8 @@ public class KubernetesClusterActionWorker {
 
     protected IpAddress getPublicIp(Network network) throws ManagementServerException {
         if (network.getVpcId() != null) {
-            IpAddress publicIp = getVpcTierKubernetesPublicIp(network);
+            String detailKey = manager.isNetrisNetwork(network) ? ApiConstants.NETRIS_NAT_PUBLIC_IP_ID : ApiConstants.PUBLIC_IP_ID;
+            IpAddress publicIp = getVpcTierKubernetesPublicIp(network, detailKey);
             if (publicIp == null) {
                 throw new ManagementServerException(String.format("No public IP addresses found for VPC tier : %s, Kubernetes cluster : %s", network.getName(), kubernetesCluster.getName()));
             }
diff --git a/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/service/NetrisApiClientImpl.java b/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/service/NetrisApiClientImpl.java
index bc6dd912be5..b053f2a854e 100644
--- a/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/service/NetrisApiClientImpl.java
+++ b/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/service/NetrisApiClientImpl.java
@@ -1763,12 +1763,12 @@ public class NetrisApiClientImpl implements NetrisApiClient {
                 IpTreeSubnet existingSubnet = matchedSubnets.stream().filter(x -> x.getPrefix().equals(natIp)).collect(Collectors.toList()).get(0);
                 if (existingSubnet.getPurpose() != IpTreeSubnet.PurposeEnum.NAT) {
                     VPCListing systemVpc = getSystemVpc();
-                    logger.debug("Subnet: {} already exists, but purpose is not NAT, updating its purpose to 'nat'", natIp);
+                    logger.debug("Subnet: {} already exists with purpose '{}', updating its purpose to 'nat'", natIp, existingSubnet.getPurpose());
                     updateIpamSubnetInternal(existingSubnet.getId().intValue(), netrisSubnetName, natIp, SubnetBody.PurposeEnum.NAT, systemVpc, null);
                 }
             }
         } catch (ApiException e) {
-            throw new CloudRuntimeException(String.format("Failed to create subnet for %s with NAT purpose", natIp));
+            logAndThrowException(String.format("Failed to create subnet for %s with NAT purpose (current purpose conflicts and could not be changed)", natIp), e);
         }
     }
 
@@ -1784,12 +1784,13 @@ public class NetrisApiClientImpl implements NetrisApiClient {
             if (matchedSubnets.isEmpty()) {
                 createIpamSubnetInternal(netrisSubnetName, lbIp, SubnetBody.PurposeEnum.LOAD_BALANCER, systemVpc, null);
             } else if (IpTreeSubnet.PurposeEnum.LOAD_BALANCER != matchedSubnets.get(0).getPurpose()){
-                logger.debug("Updating existing NAT subnet {} to have load balancer purpose", netrisSubnetName);
-                updateIpamSubnetInternal(matchedSubnets.get(0).getId().intValue(), netrisSubnetName, lbIp, SubnetBody.PurposeEnum.LOAD_BALANCER, systemVpc, null);
+                IpTreeSubnet existingSubnet = matchedSubnets.get(0);
+                logger.debug("Updating existing NAT subnet {} with purpose '{}' to have load balancer purpose", netrisSubnetName, existingSubnet.getPurpose());
+                updateIpamSubnetInternal(existingSubnet.getId().intValue(), netrisSubnetName, lbIp, SubnetBody.PurposeEnum.LOAD_BALANCER, systemVpc, null);
             }
             logger.debug("LB subnet: {} already exists", netrisSubnetName);
         } catch (ApiException e) {
-            throw new CloudRuntimeException(String.format("Failed to create subnet for %s with LB purpose", lbIp));
+            logAndThrowException(String.format("Failed to create subnet for %s with LB purpose (current purpose conflicts and could not be changed)", lbIp), e);
         }
     }
 
diff --git a/server/src/main/java/com/cloud/network/NetworkModelImpl.java b/server/src/main/java/com/cloud/network/NetworkModelImpl.java
index 07305c0e641..0e53c9a6bfb 100644
--- a/server/src/main/java/com/cloud/network/NetworkModelImpl.java
+++ b/server/src/main/java/com/cloud/network/NetworkModelImpl.java
@@ -495,7 +495,8 @@ public class NetworkModelImpl extends ManagerBase implements NetworkModel, Confi
     public Map<Provider, ArrayList<PublicIpAddress>> getProviderToIpList(Network network, Map<PublicIpAddress, Set<Service>> ipToServices) {
         NetworkOffering offering = _networkOfferingDao.findById(network.getNetworkOfferingId());
         boolean isForNsx = isProviderForNetworkOffering(Provider.Nsx, offering.getId());
-        if (!offering.isConserveMode() && !isForNsx) {
+        boolean isForNetris = isProviderForNetworkOffering(Provider.Netris, offering.getId());
+        if (!offering.isConserveMode() && !isForNsx && !isForNetris) {
             for (PublicIpAddress ip : ipToServices.keySet()) {
                 Set<Service> services = new HashSet<Service>();
                 services.addAll(ipToServices.get(ip));
@@ -1649,7 +1650,8 @@ public class NetworkModelImpl extends ManagerBase implements NetworkModel, Confi
             return false;
         }
         boolean isForNsx = isProviderForNetworkOffering(Provider.Nsx, offering.getId());
-        if (!offering.isConserveMode() && !isForNsx) {
+        boolean isForNetris = isProviderForNetworkOffering(Provider.Netris, offering.getId());
+        if (!offering.isConserveMode() && !isForNsx && !isForNetris) {
             return canIpUsedForNonConserveService(publicIp, service);
         }
         return true;


(cloudstack) 02/08: Add a check to prevent re-adding existing ACLs

Posted by pe...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

Pearl1594 pushed a commit to branch netris-improvements
in repository https://gitbox.apache.org/repos/asf/cloudstack.git

commit 1618383cb7da88f1384c330380026f8cf05eeb7f
Author: Pearl Dsilva <pe...@gmail.com>
AuthorDate: Wed Jun 24 10:46:45 2026 -0400

    Add a check to prevent re-adding existing ACLs
    
    * Netris - Support NAT, PF for Secondary IP
    
    * Allow PF rules to be correctly created on secondary IP of a VM on netris
    
    * unique static nat names on netris by adding public IP - handle backward compatibility
    
    ---------
    
    Co-authored-by: Pearl Dsilva <pe...@gmail.com>
    
    * Netris - Support for L2 networks
    
    * Add support for L2 networks in Netris
    
    * remove the need to add connectivity to identify netris provider, remove changes to ui and create net offering
    
    * create vpc network and use it for l2 networks
    
    * dont fail if deletion on netris doesnt succeed
    
    * remove connectivity capability
    
    * update vpc name when l2 name is updated
    
    ---------
    
    Co-authored-by: Pearl Dsilva <pe...@gmail.com>
    
    * Netris - CKS support
    
    Co-authored-by: Pearl Dsilva <pe...@gmail.com>
    
    * Netris - Enable Global Routing Flag for NATTED Dual-Stack VPCs
    
    * Set globalRouting = true, when network offering is NATTED, dual-stack
    
    * update global routing based on routing mode and ip protocol type
    
    ---------
    
    Co-authored-by: Pearl Dsilva <pe...@gmail.com>
    Co-authored-by: nvazquez <ni...@gmail.com>
    
    * Netris - Fix allocate vnets with the correct account ID so that release succeeds on network deletion
    
    * Fix allocate vnets with the correct account ID so that release succeeds on network deletion
    
    * add fix for vpc tiers as well
    
    * move entire vnet cleanup logic to trash from shutdown, so that it works for all network types for netris
    
    * cleanup vnets when reservation id is null
    
    ---------
    
    Co-authored-by: Pearl Dsilva <pe...@gmail.com>
    
    * Netris - Add support to include VPN service for NATTED Netris offerings
    
    * Add support to include VPN service for NATTED Netris offerings
    
    * missing changes of global routing
    
    * Remove extra space
    
    ---------
    
    Co-authored-by: Pearl Dsilva <pe...@gmail.com>
    Co-authored-by: nvazquez <ni...@gmail.com>
    
    * Netris - Support Redundant Virtual Routers for Netris VPC networks
    
    * Add support for redundant routers for Netris VPC networks
    
    * address comment
    
    * default null network mode to natted for netris
    
    * preventing vpc and ipam creation if already exists
    
    ---------
    
    Co-authored-by: Pearl Dsilva <pe...@gmail.com>
    
    * Netris - Enable autoscaling groups for Netris network offerings
    
    * Enable autoscaling groups for Netris network offerings
    
    * Fix UI loading for services when network mode changes for external providers and show vm autoscaling always set to true / non-editable for netris when LB is selected i.e, natted mode
    
    * Revert removed comment
    
    ---------
    
    Co-authored-by: Pearl Dsilva <pe...@gmail.com>
    Co-authored-by: nvazquez <ni...@gmail.com>
    
    * merge conflict
    
    * systemvm: fix VPC VPN issue when network id is bigger than 1000 on Netris
    
    * show networks for vnf nic mappings
    
    * Add a check to prevent re-adding existing ACLs
    
    * Add a check to prevent re-adding existing ACLs
    
    * change message compare statement
    
    ---------
    
    Co-authored-by: Pearl Dsilva <pe...@gmail.com>
    Co-authored-by: nvazquez <ni...@gmail.com>
    Co-authored-by: Wei Zhou <we...@apache.org>
---
 .../cloudstack/service/NetrisApiClientImpl.java     | 21 ++++++++++++++-------
 1 file changed, 14 insertions(+), 7 deletions(-)

diff --git a/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/service/NetrisApiClientImpl.java b/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/service/NetrisApiClientImpl.java
index c4598ddfec4..cb4ed59333b 100644
--- a/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/service/NetrisApiClientImpl.java
+++ b/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/service/NetrisApiClientImpl.java
@@ -438,7 +438,7 @@ public class NetrisApiClientImpl implements NetrisApiClient {
                 try {
                     aclApi.apiAclPut(aclEditItem);
                 } catch (ApiException e) {
-                    if (e.getResponseBody().contains("This kind of acl already exists")) {
+                    if (e.getResponseBody().contains("already exists")) {
                         logger.info("Netris ACL rule: {} already exists and doesn't need to be updated", aclName);
                         return true;
                     }
@@ -448,7 +448,15 @@ public class NetrisApiClientImpl implements NetrisApiClient {
             }
             AclAddItem aclAddItem = getAclAddItem(cmd, aclName);
             aclAddItem.setVpc(vpc);
-            aclApi.apiAclPost(aclAddItem);
+            try {
+                aclApi.apiAclPost(aclAddItem);
+            } catch (ApiException e) {
+                if (e.getResponseBody() != null && e.getResponseBody().contains("already exists")) {
+                    logger.info("Netris ACL rule: {} already exists, skipping creation", aclName);
+                    return true;
+                }
+                throw e;
+            }
         } catch (ApiException e) {
             logAndThrowException(String.format("Failed to create Netris ACL: %s", cmd.getNetrisAclName()), e);
         }
@@ -971,13 +979,12 @@ public class NetrisApiClientImpl implements NetrisApiClient {
                 logger.error("Could not find the Netris LB rule with name {}", lbName);
                 return false;
             }
-            if (matchingLbId.isEmpty()) {
+            if (!matchingLbId.isEmpty()) {
+                L4LoadBalancerApi lbApi = apiClient.getApiStubForMethod(L4LoadBalancerApi.class);
+                lbApi.apiV2L4lbIdDelete(matchingLbId.get(0).intValue());
+            } else {
                 logger.warn("There doesn't seem to be any LB rule on Netris matching {}", lbName);
-                return true;
             }
-
-            L4LoadBalancerApi lbApi = apiClient.getApiStubForMethod(L4LoadBalancerApi.class);
-            lbApi.apiV2L4lbIdDelete(matchingLbId.get(0).intValue());
             if (Objects.nonNull(cidrList)) {
                 deleteAclRulesForLb(cmd);
             }