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

Pearl1594 pushed a commit to branch support-multi-vlan-guestnet
in repository https://gitbox.apache.org/repos/asf/cloudstack.git

commit 749b2b7f005b7a357e6458f19a5ca2667fb34d52
Author: Pearl1594 <[email protected]>
AuthorDate: Tue Oct 6 14:13:09 2026 -0400

    Fix DHCP databag merge clobbering a trunk nic's sibling entry on shared 
VPC-tier VRs
---
 .../cloud/agent/api/routing/DhcpEntryCommand.java    | 12 ++++++++++++
 .../virtualnetwork/facade/DhcpEntryConfigItem.java   |  2 +-
 .../resource/virtualnetwork/model/VmDhcpConfig.java  | 16 ++++++++++++++--
 .../com/cloud/network/router/CommandSetupHelper.java | 12 ++++++++++--
 systemvm/debian/opt/cloud/bin/cs_dhcp.py             | 20 +++++++++++++-------
 5 files changed, 50 insertions(+), 12 deletions(-)

diff --git 
a/core/src/main/java/com/cloud/agent/api/routing/DhcpEntryCommand.java 
b/core/src/main/java/com/cloud/agent/api/routing/DhcpEntryCommand.java
index 67ca447e385..cd7ab226fa7 100644
--- a/core/src/main/java/com/cloud/agent/api/routing/DhcpEntryCommand.java
+++ b/core/src/main/java/com/cloud/agent/api/routing/DhcpEntryCommand.java
@@ -34,6 +34,10 @@ public class DhcpEntryCommand extends NetworkElementCommand {
     String ip6Gateway;
     String duid;
     private boolean isDefault;
+    // True only for a nic's associated-network entry (multi-VLAN trunk nics) 
- distinct from isDefault,
+    // which reflects whether the nic is the VM's default nic, not whether 
this entry may coexist with
+    // other entries sharing the same MAC. See cs_dhcp.py's merge() on the VR 
for how this is consumed.
+    private boolean associatedNetwork;
     boolean executeInSequence = false;
     boolean remove;
     Long leaseTime;
@@ -154,6 +158,14 @@ public class DhcpEntryCommand extends 
NetworkElementCommand {
         this.isDefault = isDefault;
     }
 
+    public boolean isAssociatedNetwork() {
+        return associatedNetwork;
+    }
+
+    public void setAssociatedNetwork(boolean associatedNetwork) {
+        this.associatedNetwork = associatedNetwork;
+    }
+
     public Long getLeaseTime() {
         return leaseTime;
     }
diff --git 
a/core/src/main/java/com/cloud/agent/resource/virtualnetwork/facade/DhcpEntryConfigItem.java
 
b/core/src/main/java/com/cloud/agent/resource/virtualnetwork/facade/DhcpEntryConfigItem.java
index 041645046b2..d9faec0443e 100644
--- 
a/core/src/main/java/com/cloud/agent/resource/virtualnetwork/facade/DhcpEntryConfigItem.java
+++ 
b/core/src/main/java/com/cloud/agent/resource/virtualnetwork/facade/DhcpEntryConfigItem.java
@@ -35,7 +35,7 @@ public class DhcpEntryConfigItem extends 
AbstractConfigItemFacade {
         final DhcpEntryCommand command = (DhcpEntryCommand) cmd;
 
         final VmDhcpConfig vmDhcpConfig = new 
VmDhcpConfig(command.getVmName(), command.getVmMac(), command.getVmIpAddress(), 
command.getVmIp6Address(), command.getDuid(), command.getDefaultDns(),
-                command.getDefaultRouter(), command.getStaticRoutes(), 
command.isDefault(), command.isRemove(), command.getLeaseTime());
+                command.getDefaultRouter(), command.getStaticRoutes(), 
command.isDefault(), command.isRemove(), command.getLeaseTime(), 
command.isAssociatedNetwork());
 
         return generateConfigItems(vmDhcpConfig);
     }
diff --git 
a/core/src/main/java/com/cloud/agent/resource/virtualnetwork/model/VmDhcpConfig.java
 
b/core/src/main/java/com/cloud/agent/resource/virtualnetwork/model/VmDhcpConfig.java
index 1c43cd1823b..d51c05b0a70 100644
--- 
a/core/src/main/java/com/cloud/agent/resource/virtualnetwork/model/VmDhcpConfig.java
+++ 
b/core/src/main/java/com/cloud/agent/resource/virtualnetwork/model/VmDhcpConfig.java
@@ -29,6 +29,9 @@ public class VmDhcpConfig extends ConfigBase {
     private String defaultGateway;
     private String staticRoutes;
     private boolean defaultEntry;
+    // True only for a nic's associated-network entry (multi-VLAN trunk nics) 
- lets the VR-side merge
+    // keep this entry from overwriting, or being overwritten by, another 
entry sharing the same MAC.
+    private boolean associatedNetwork;
     private Long leaseTime;
 
     // Indicate if the entry should be removed when set to true
@@ -40,11 +43,11 @@ public class VmDhcpConfig extends ConfigBase {
 
     public VmDhcpConfig(String hostName, String macAddress, String 
ipv4Address, String ipv6Address, String ipv6Duid, String dnsAddresses, String 
defaultGateway,
             String staticRoutes, boolean defaultEntry, boolean remove) {
-        this(hostName, macAddress, ipv4Address, ipv6Address, ipv6Duid, 
dnsAddresses, defaultGateway, staticRoutes, defaultEntry, remove, null);
+        this(hostName, macAddress, ipv4Address, ipv6Address, ipv6Duid, 
dnsAddresses, defaultGateway, staticRoutes, defaultEntry, remove, null, false);
     }
 
     public VmDhcpConfig(String hostName, String macAddress, String 
ipv4Address, String ipv6Address, String ipv6Duid, String dnsAddresses, String 
defaultGateway,
-            String staticRoutes, boolean defaultEntry, boolean remove, Long 
leaseTime) {
+            String staticRoutes, boolean defaultEntry, boolean remove, Long 
leaseTime, boolean associatedNetwork) {
         super(VM_DHCP);
         this.hostName = hostName;
         this.macAddress = macAddress;
@@ -57,6 +60,7 @@ public class VmDhcpConfig extends ConfigBase {
         this.defaultEntry = defaultEntry;
         this.remove = remove;
         this.leaseTime = leaseTime;
+        this.associatedNetwork = associatedNetwork;
     }
 
     public String getHostName() {
@@ -147,4 +151,12 @@ public class VmDhcpConfig extends ConfigBase {
         this.leaseTime = leaseTime;
     }
 
+    public boolean isAssociatedNetwork() {
+        return associatedNetwork;
+    }
+
+    public void setAssociatedNetwork(boolean associatedNetwork) {
+        this.associatedNetwork = associatedNetwork;
+    }
+
 }
diff --git 
a/server/src/main/java/com/cloud/network/router/CommandSetupHelper.java 
b/server/src/main/java/com/cloud/network/router/CommandSetupHelper.java
index d30b3968aca..2fb10b622b6 100644
--- a/server/src/main/java/com/cloud/network/router/CommandSetupHelper.java
+++ b/server/src/main/java/com/cloud/network/router/CommandSetupHelper.java
@@ -285,14 +285,21 @@ public class CommandSetupHelper {
 
     public void createDhcpEntryCommand(final VirtualRouter router, final 
UserVm vm, final NicVO nic, boolean remove, final Commands cmds) {
         createDhcpEntryCommand(router, vm, nic.getMacAddress(), 
nic.getIPv4Address(), nic.getIPv6Address(), nic.getIPv4Gateway(), 
nic.getIPv6Gateway(),
-                nic.getNetworkId(), nic.isDefaultNic(), remove, cmds);
+                nic.getNetworkId(), nic.isDefaultNic(), remove, false, cmds);
     }
 
     // Same as createDhcpEntryCommand(router, vm, NicVO, ...), but for a nic's 
association with a network other
     // than its primary (multi-VLAN trunk nics) - takes the addressing 
explicitly, since a nic's own DB row only
-    // ever carries its primary network's IP/gateway.
+    // ever carries its primary network's IP/gateway. Always called with 
isAssociatedNetwork=true: this is the
+    // one overload every trunk-association DHCP push goes through, directly 
(never via the NicVO overload above).
     public void createDhcpEntryCommand(final VirtualRouter router, final 
UserVm vm, final String macAddress, final String ip4Address, final String 
ip6Address,
             final String ip4Gateway, final String ip6Gateway, final long 
networkId, final boolean isDefaultNic, boolean remove, final Commands cmds) {
+        createDhcpEntryCommand(router, vm, macAddress, ip4Address, ip6Address, 
ip4Gateway, ip6Gateway, networkId, isDefaultNic, remove, true, cmds);
+    }
+
+    private void createDhcpEntryCommand(final VirtualRouter router, final 
UserVm vm, final String macAddress, final String ip4Address, final String 
ip6Address,
+            final String ip4Gateway, final String ip6Gateway, final long 
networkId, final boolean isDefaultNic, boolean remove, final boolean 
isAssociatedNetwork,
+            final Commands cmds) {
         final DhcpEntryCommand dhcpCommand = new DhcpEntryCommand(macAddress, 
ip4Address, vm.getHostName(), ip6Address,
                 _networkModel.getExecuteInSeqNtwkElmtCmd());
 
@@ -308,6 +315,7 @@ public class CommandSetupHelper {
         dhcpCommand.setDefaultDns(ipaddress);
         dhcpCommand.setDuid(NetUtils.getDuidLL(macAddress));
         dhcpCommand.setDefault(isDefaultNic);
+        dhcpCommand.setAssociatedNetwork(isAssociatedNetwork);
         dhcpCommand.setRemove(remove);
 
         // Set DHCP lease timeout from zone-scoped config (0 = infinite)
diff --git a/systemvm/debian/opt/cloud/bin/cs_dhcp.py 
b/systemvm/debian/opt/cloud/bin/cs_dhcp.py
index cd6574fd926..cfebd125438 100755
--- a/systemvm/debian/opt/cloud/bin/cs_dhcp.py
+++ b/systemvm/debian/opt/cloud/bin/cs_dhcp.py
@@ -26,14 +26,20 @@ def merge(dbag, data):
         if data['ipv4_address'] in dbag:
             del dbag[data['ipv4_address']]
     else:
-        remove_keys = set()
-        for key, entry in dbag.items():
-            if key != 'id' and entry['mac_address'] == data['mac_address']:
-                remove_keys.add(key)
-                break
+        # A multi-VLAN trunk nic's one MAC can legitimately hold several 
simultaneous entries - its
+        # primary network's, plus one per associated network. Only ever 
replace an existing entry for
+        # this MAC when the incoming entry is itself not an associated-network 
entry, and only replace
+        # an existing entry that is also not one - otherwise an association's 
push would delete the
+        # nic's own primary entry (or a sibling association's), and vice versa.
+        if not data.get('associated_network', False):
+            remove_keys = set()
+            for key, entry in dbag.items():
+                if key != 'id' and entry['mac_address'] == data['mac_address'] 
and not entry.get('associated_network', False):
+                    remove_keys.add(key)
+                    break
 
-        for remove_key in remove_keys:
-            del dbag[remove_key]
+            for remove_key in remove_keys:
+                del dbag[remove_key]
 
         dbag[data['ipv4_address']] = data
 

Reply via email to