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 baebacedeef34271e612c1f80023d4774d3d9cc8
Author: Pearl1594 <[email protected]>
AuthorDate: Mon Sep 14 15:14:34 2026 -0400

    Support multi-VLAN trunk NICs
---
 api/src/main/java/com/cloud/host/Host.java         |   2 +
 api/src/main/java/com/cloud/vm/Nic.java            |   2 +
 .../org/apache/cloudstack/api/ApiConstants.java    |   2 +
 .../cloudstack/api/response/HostResponse.java      |  24 ++++
 .../com/cloud/agent/manager/AgentManagerImpl.java  |  12 +-
 .../schema/src/main/java/com/cloud/vm/NicVO.java   |  14 ++-
 .../java/com/cloud/vm/dao/NicNetworkMapDao.java    |  32 +++++
 .../com/cloud/vm/dao/NicNetworkMapDaoImpl.java     |  77 ++++++++++++
 .../java/com/cloud/vm/dao/NicNetworkMapVO.java     | 140 +++++++++++++++++++++
 .../spring-engine-schema-core-daos-context.xml     |   1 +
 .../resources/META-INF/db/schema-42300to2400.sql   |  22 ++++
 .../kvm/resource/LibvirtComputingResource.java     |  41 ++++++
 .../wrapper/LibvirtReadyCommandWrapper.java        |   3 +
 .../com/cloud/api/query/dao/HostJoinDaoImpl.java   |   2 +
 14 files changed, 372 insertions(+), 2 deletions(-)

diff --git a/api/src/main/java/com/cloud/host/Host.java 
b/api/src/main/java/com/cloud/host/Host.java
index c110e4ca94e..3cf36db8ad5 100644
--- a/api/src/main/java/com/cloud/host/Host.java
+++ b/api/src/main/java/com/cloud/host/Host.java
@@ -64,6 +64,8 @@ public interface Host extends StateObject<Status>, Identity, 
Partition, HAResour
     String HOST_VIRTV2V_VERSION = "host.virtv2v.version";
     String HOST_SSH_PORT = "host.ssh.port";
     String HOST_CDROM_MAX_COUNT = "host.cdrom.max.count";
+    String HOST_VLAN_FILTERING_ENABLED = "vlan.filtering.enabled";
+    String HOST_VLAN_TRUNK_XML_SUPPORTED = "vlan.trunk.xml.supported";
     String GUEST_OS_CATEGORY_ID = "guest.os.category.id";
     String GUEST_OS_RULE = "guest.os.rule";
 
diff --git a/api/src/main/java/com/cloud/vm/Nic.java 
b/api/src/main/java/com/cloud/vm/Nic.java
index 3722e5769c9..71612cd355f 100644
--- a/api/src/main/java/com/cloud/vm/Nic.java
+++ b/api/src/main/java/com/cloud/vm/Nic.java
@@ -146,6 +146,8 @@ public interface Nic extends Identity, InternalIdentity {
 
     boolean getSecondaryIp();
 
+    boolean getMultiNetwork();
+
     //
     // IPv4
     //
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 f74c4616118..de6aeed50a0 100644
--- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java
+++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java
@@ -318,6 +318,8 @@ public class ApiConstants {
     public static final String MOVE_ACL_CONSISTENCY_HASH = 
"aclconsistencyhash";
     public static final String IMAGE_PATH = "imagepath";
     public static final String INSTANCE_CONVERSION_SUPPORTED = 
"instanceconversionsupported";
+    public static final String VLAN_FILTERING_ENABLED = "vlanfilteringenabled";
+    public static final String VLAN_TRUNK_XML_SUPPORTED = 
"vlantrunkxmlsupported";
     public static final String INTERNAL_DNS1 = "internaldns1";
     public static final String INTERNAL_DNS2 = "internaldns2";
     public static final String INTERNET_PROTOCOL = "internetprotocol";
diff --git 
a/api/src/main/java/org/apache/cloudstack/api/response/HostResponse.java 
b/api/src/main/java/org/apache/cloudstack/api/response/HostResponse.java
index 10bd62804fb..52804cc980c 100644
--- a/api/src/main/java/org/apache/cloudstack/api/response/HostResponse.java
+++ b/api/src/main/java/org/apache/cloudstack/api/response/HostResponse.java
@@ -315,6 +315,14 @@ public class HostResponse extends 
BaseResponseWithAnnotations {
     @Param(description = "true if the host supports instance conversion (using 
virt-v2v)", since = "4.19.1")
     private Boolean instanceConversionSupported;
 
+    @SerializedName(ApiConstants.VLAN_FILTERING_ENABLED)
+    @Param(description = "true if vlan_filtering is enabled on this host's 
guest bridge, so it can accept multi-VLAN trunk nics", since = "24.0.0")
+    private Boolean vlanFilteringEnabled;
+
+    @SerializedName(ApiConstants.VLAN_TRUNK_XML_SUPPORTED)
+    @Param(description = "true if this host's libvirt version supports trunk 
vlan tap membership natively", since = "24.0.0")
+    private Boolean vlanTrunkXmlSupported;
+
     @SerializedName(ApiConstants.ARCH)
     @Param(description = "CPU Arch of the host", since = "4.20")
     private String arch;
@@ -904,6 +912,14 @@ public class HostResponse extends 
BaseResponseWithAnnotations {
         this.instanceConversionSupported = instanceConversionSupported;
     }
 
+    public void setVlanFilteringEnabled(Boolean vlanFilteringEnabled) {
+        this.vlanFilteringEnabled = vlanFilteringEnabled;
+    }
+
+    public void setVlanTrunkXmlSupported(Boolean vlanTrunkXmlSupported) {
+        this.vlanTrunkXmlSupported = vlanTrunkXmlSupported;
+    }
+
     public Boolean getIsTagARule() {
         return isTagARule;
     }
@@ -1000,6 +1016,14 @@ public class HostResponse extends 
BaseResponseWithAnnotations {
         return instanceConversionSupported;
     }
 
+    public Boolean getVlanFilteringEnabled() {
+        return vlanFilteringEnabled;
+    }
+
+    public Boolean getVlanTrunkXmlSupported() {
+        return vlanTrunkXmlSupported;
+    }
+
     public void setExtensionId(String extensionId) {
         this.extensionId = extensionId;
     }
diff --git 
a/engine/orchestration/src/main/java/com/cloud/agent/manager/AgentManagerImpl.java
 
b/engine/orchestration/src/main/java/com/cloud/agent/manager/AgentManagerImpl.java
index 1215829d92f..eede907b187 100644
--- 
a/engine/orchestration/src/main/java/com/cloud/agent/manager/AgentManagerImpl.java
+++ 
b/engine/orchestration/src/main/java/com/cloud/agent/manager/AgentManagerImpl.java
@@ -808,8 +808,10 @@ public class AgentManagerImpl extends ManagerBase 
implements AgentManager, Handl
                 String vddkSupport = detailsMap.get(Host.HOST_VDDK_SUPPORT);
                 String vddkLibDir = detailsMap.get(Host.HOST_VDDK_LIB_DIR);
                 String vddkVersion = detailsMap.get(Host.HOST_VDDK_VERSION);
+                String vlanFilteringEnabled = 
detailsMap.get(Host.HOST_VLAN_FILTERING_ENABLED);
+                String vlanTrunkXmlSupported = 
detailsMap.get(Host.HOST_VLAN_TRUNK_XML_SUPPORTED);
                 logger.debug("Got HOST_UEFI_ENABLE [{}] for host [{}]:", 
uefiEnabled, host);
-                if (ObjectUtils.anyNotNull(uefiEnabled, virtv2vVersion, 
ovftoolVersion, vddkSupport, vddkLibDir, vddkVersion)) {
+                if (ObjectUtils.anyNotNull(uefiEnabled, virtv2vVersion, 
ovftoolVersion, vddkSupport, vddkLibDir, vddkVersion, vlanFilteringEnabled, 
vlanTrunkXmlSupported)) {
                     _hostDao.loadDetails(host);
                     boolean updateNeeded = false;
                     if (StringUtils.isNotBlank(uefiEnabled) && 
!uefiEnabled.equals(host.getDetails().get(Host.HOST_UEFI_ENABLE))) {
@@ -828,6 +830,14 @@ public class AgentManagerImpl extends ManagerBase 
implements AgentManager, Handl
                         host.getDetails().put(Host.HOST_VDDK_SUPPORT, 
vddkSupport);
                         updateNeeded = true;
                     }
+                    if (StringUtils.isNotBlank(vlanFilteringEnabled) && 
!vlanFilteringEnabled.equals(host.getDetails().get(Host.HOST_VLAN_FILTERING_ENABLED)))
 {
+                        
host.getDetails().put(Host.HOST_VLAN_FILTERING_ENABLED, vlanFilteringEnabled);
+                        updateNeeded = true;
+                    }
+                    if (StringUtils.isNotBlank(vlanTrunkXmlSupported) && 
!vlanTrunkXmlSupported.equals(host.getDetails().get(Host.HOST_VLAN_TRUNK_XML_SUPPORTED)))
 {
+                        
host.getDetails().put(Host.HOST_VLAN_TRUNK_XML_SUPPORTED, 
vlanTrunkXmlSupported);
+                        updateNeeded = true;
+                    }
                     if 
(!StringUtils.defaultString(vddkLibDir).equals(StringUtils.defaultString(host.getDetails().get(Host.HOST_VDDK_LIB_DIR))))
 {
                         if (StringUtils.isBlank(vddkLibDir)) {
                             host.getDetails().remove(Host.HOST_VDDK_LIB_DIR);
diff --git a/engine/schema/src/main/java/com/cloud/vm/NicVO.java 
b/engine/schema/src/main/java/com/cloud/vm/NicVO.java
index 65946b8d821..bcda4f78775 100644
--- a/engine/schema/src/main/java/com/cloud/vm/NicVO.java
+++ b/engine/schema/src/main/java/com/cloud/vm/NicVO.java
@@ -128,6 +128,9 @@ public class NicVO implements Nic {
     @Column(name = "secondary_ip")
     boolean secondaryIp;
 
+    @Column(name = "multi_network")
+    boolean multiNetwork;
+
     @Column(name = "mtu")
     Integer mtu;
 
@@ -337,7 +340,7 @@ public class NicVO implements Nic {
         return String.format("Nic %s",
                 ReflectionToStringBuilderUtils.reflectOnlySelectedFields(
                         this, "id", "uuid", "instanceId",
-                        "deviceId", "broadcastUri", "reservationId", 
"iPv4Address"));
+                        "deviceId", "broadcastUri", "reservationId", 
"iPv4Address", "multiNetwork"));
     }
 
     @Override
@@ -381,6 +384,15 @@ public class NicVO implements Nic {
         this.secondaryIp = secondaryIp;
     }
 
+    @Override
+    public boolean getMultiNetwork() {
+        return multiNetwork;
+    }
+
+    public void setMultiNetwork(boolean multiNetwork) {
+        this.multiNetwork = multiNetwork;
+    }
+
     public void setVmType(VirtualMachine.Type vmType) {
         this.vmType = vmType;
     }
diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/NicNetworkMapDao.java 
b/engine/schema/src/main/java/com/cloud/vm/dao/NicNetworkMapDao.java
new file mode 100644
index 00000000000..b5a1ed3137a
--- /dev/null
+++ b/engine/schema/src/main/java/com/cloud/vm/dao/NicNetworkMapDao.java
@@ -0,0 +1,32 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+package com.cloud.vm.dao;
+
+import java.util.List;
+
+import com.cloud.utils.db.GenericDao;
+
+public interface NicNetworkMapDao extends GenericDao<NicNetworkMapVO, Long> {
+
+    List<NicNetworkMapVO> listByNicId(long nicId);
+
+    List<NicNetworkMapVO> listByNetworkId(long networkId);
+
+    NicNetworkMapVO findByNicIdAndNetworkId(long nicId, long networkId);
+
+    List<Long> listNicIdsByNetworkId(long networkId);
+}
diff --git 
a/engine/schema/src/main/java/com/cloud/vm/dao/NicNetworkMapDaoImpl.java 
b/engine/schema/src/main/java/com/cloud/vm/dao/NicNetworkMapDaoImpl.java
new file mode 100644
index 00000000000..f905809350e
--- /dev/null
+++ b/engine/schema/src/main/java/com/cloud/vm/dao/NicNetworkMapDaoImpl.java
@@ -0,0 +1,77 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+package com.cloud.vm.dao;
+
+import java.util.List;
+
+import org.springframework.stereotype.Component;
+
+import com.cloud.utils.db.GenericDaoBase;
+import com.cloud.utils.db.GenericSearchBuilder;
+import com.cloud.utils.db.SearchBuilder;
+import com.cloud.utils.db.SearchCriteria;
+import com.cloud.utils.db.SearchCriteria.Func;
+import com.cloud.utils.db.SearchCriteria.Op;
+
+@Component
+public class NicNetworkMapDaoImpl extends GenericDaoBase<NicNetworkMapVO, 
Long> implements NicNetworkMapDao {
+
+    private final SearchBuilder<NicNetworkMapVO> AllFieldsSearch;
+    private final GenericSearchBuilder<NicNetworkMapVO, Long> 
NicIdsByNetworkSearch;
+
+    public NicNetworkMapDaoImpl() {
+        super();
+        AllFieldsSearch = createSearchBuilder();
+        AllFieldsSearch.and("nicId", AllFieldsSearch.entity().getNicId(), 
Op.EQ);
+        AllFieldsSearch.and("networkId", 
AllFieldsSearch.entity().getNetworkId(), Op.EQ);
+        AllFieldsSearch.done();
+
+        NicIdsByNetworkSearch = createSearchBuilder(Long.class);
+        NicIdsByNetworkSearch.select(null, Func.DISTINCT, 
NicIdsByNetworkSearch.entity().getNicId());
+        NicIdsByNetworkSearch.and("networkId", 
NicIdsByNetworkSearch.entity().getNetworkId(), Op.EQ);
+        NicIdsByNetworkSearch.done();
+    }
+
+    @Override
+    public List<NicNetworkMapVO> listByNicId(long nicId) {
+        SearchCriteria<NicNetworkMapVO> sc = AllFieldsSearch.create();
+        sc.setParameters("nicId", nicId);
+        return listBy(sc);
+    }
+
+    @Override
+    public List<NicNetworkMapVO> listByNetworkId(long networkId) {
+        SearchCriteria<NicNetworkMapVO> sc = AllFieldsSearch.create();
+        sc.setParameters("networkId", networkId);
+        return listBy(sc);
+    }
+
+    @Override
+    public NicNetworkMapVO findByNicIdAndNetworkId(long nicId, long networkId) 
{
+        SearchCriteria<NicNetworkMapVO> sc = AllFieldsSearch.create();
+        sc.setParameters("nicId", nicId);
+        sc.setParameters("networkId", networkId);
+        return findOneBy(sc);
+    }
+
+    @Override
+    public List<Long> listNicIdsByNetworkId(long networkId) {
+        SearchCriteria<Long> sc = NicIdsByNetworkSearch.create();
+        sc.setParameters("networkId", networkId);
+        return customSearch(sc, null);
+    }
+}
diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/NicNetworkMapVO.java 
b/engine/schema/src/main/java/com/cloud/vm/dao/NicNetworkMapVO.java
new file mode 100644
index 00000000000..79344535460
--- /dev/null
+++ b/engine/schema/src/main/java/com/cloud/vm/dao/NicNetworkMapVO.java
@@ -0,0 +1,140 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+package com.cloud.vm.dao;
+
+import java.util.Date;
+import java.util.UUID;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+import javax.persistence.Table;
+
+import org.apache.cloudstack.api.Identity;
+import org.apache.cloudstack.api.InternalIdentity;
+import 
org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils;
+
+import com.cloud.utils.db.GenericDao;
+
+@Entity
+@Table(name = "nic_network_map")
+public class NicNetworkMapVO implements Identity, InternalIdentity {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    @Column(name = "id")
+    private long id;
+
+    @Column(name = "uuid")
+    private String uuid = UUID.randomUUID().toString();
+
+    @Column(name = "nic_id")
+    private long nicId;
+
+    @Column(name = "network_id")
+    private long networkId;
+
+    @Column(name = "ip4_address")
+    private String ip4Address;
+
+    @Column(name = "ip6_address")
+    private String ip6Address;
+
+    @Column(name = GenericDao.CREATED_COLUMN)
+    private Date created;
+
+    @Column(name = GenericDao.REMOVED_COLUMN)
+    private Date removed;
+
+    protected NicNetworkMapVO() {
+    }
+
+    public NicNetworkMapVO(long nicId, long networkId) {
+        this.nicId = nicId;
+        this.networkId = networkId;
+    }
+
+    public NicNetworkMapVO(long nicId, long networkId, String ip4Address, 
String ip6Address) {
+        this.nicId = nicId;
+        this.networkId = networkId;
+        this.ip4Address = ip4Address;
+        this.ip6Address = ip6Address;
+    }
+
+    @Override
+    public long getId() {
+        return id;
+    }
+
+    @Override
+    public String getUuid() {
+        return uuid;
+    }
+
+    public void setUuid(String uuid) {
+        this.uuid = uuid;
+    }
+
+    public long getNicId() {
+        return nicId;
+    }
+
+    public void setNicId(long nicId) {
+        this.nicId = nicId;
+    }
+
+    public long getNetworkId() {
+        return networkId;
+    }
+
+    public void setNetworkId(long networkId) {
+        this.networkId = networkId;
+    }
+
+    public String getIp4Address() {
+        return ip4Address;
+    }
+
+    public void setIp4Address(String ip4Address) {
+        this.ip4Address = ip4Address;
+    }
+
+    public String getIp6Address() {
+        return ip6Address;
+    }
+
+    public void setIp6Address(String ip6Address) {
+        this.ip6Address = ip6Address;
+    }
+
+    public Date getCreated() {
+        return created;
+    }
+
+    public Date getRemoved() {
+        return removed;
+    }
+
+    @Override
+    public String toString() {
+        return String.format("NicNetworkMap %s",
+                ReflectionToStringBuilderUtils.reflectOnlySelectedFields(
+                        this, "id", "uuid", "nicId", "networkId", 
"ip4Address", "ip6Address"));
+    }
+}
diff --git 
a/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml
 
b/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml
index 932db538f30..e89d87242b2 100644
--- 
a/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml
+++ 
b/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml
@@ -135,6 +135,7 @@
   <bean id="networkServiceMapDaoImpl" 
class="com.cloud.network.dao.NetworkServiceMapDaoImpl" />
   <bean id="nicDetailsDaoImpl" class="com.cloud.vm.dao.NicDetailsDaoImpl" />
   <bean id="nicExtraDhcpOptionDaoImpl" 
class="com.cloud.vm.dao.NicExtraDhcpOptionDaoImpl" />
+  <bean id="nicNetworkMapDaoImpl" 
class="com.cloud.vm.dao.NicNetworkMapDaoImpl" />
   <bean id="nicSecondaryIpDaoImpl" 
class="com.cloud.vm.dao.NicSecondaryIpDaoImpl" />
   <bean id="nicIpAliasDaoImpl" class="com.cloud.vm.dao.NicIpAliasDaoImpl" />
   <bean id="objectInDataStoreDaoImpl" 
class="org.apache.cloudstack.storage.db.ObjectInDataStoreDaoImpl" />
diff --git 
a/engine/schema/src/main/resources/META-INF/db/schema-42300to2400.sql 
b/engine/schema/src/main/resources/META-INF/db/schema-42300to2400.sql
index 7c11013a17d..aaedd564b77 100644
--- a/engine/schema/src/main/resources/META-INF/db/schema-42300to2400.sql
+++ b/engine/schema/src/main/resources/META-INF/db/schema-42300to2400.sql
@@ -18,3 +18,25 @@
 --;
 -- Schema upgrade from 4.23.0.0 to 24.0.0
 --;
+
+-- Multi-VLAN trunk nics: a nic may be associated with additional networks 
beyond its primary nics.network_id.
+CREATE TABLE IF NOT EXISTS `cloud`.`nic_network_map` (
+  `id` bigint unsigned NOT NULL UNIQUE AUTO_INCREMENT,
+  `uuid` varchar(40),
+  `nic_id` bigint unsigned NOT NULL COMMENT 'nic this association belongs to',
+  `network_id` bigint unsigned NOT NULL COMMENT 'additional network this nic 
is associated with',
+  `ip4_address` char(40) COMMENT 'ip4 address assigned to this nic from this 
network',
+  `ip6_address` char(40) COMMENT 'ip6 address assigned to this nic from this 
network',
+  `created` datetime NOT NULL COMMENT 'date created',
+  `removed` datetime COMMENT 'date removed if not null',
+  PRIMARY KEY (`id`),
+  CONSTRAINT `fk_nic_network_map__nic_id` FOREIGN KEY (`nic_id`) REFERENCES 
`nics`(`id`) ON DELETE CASCADE,
+  CONSTRAINT `fk_nic_network_map__network_id` FOREIGN KEY (`network_id`) 
REFERENCES `networks`(`id`),
+  CONSTRAINT `uc_nic_network_map__uuid` UNIQUE (`uuid`),
+  UNIQUE KEY `uk_nic_network_map__nic_id_network_id` (`nic_id`, `network_id`),
+  INDEX `i_nic_network_map__nic_id` (`nic_id`),
+  INDEX `i_nic_network_map__network_id` (`network_id`),
+  INDEX `i_nic_network_map__removed` (`removed`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.nics', 'multi_network', 
'tinyint(1) NOT NULL DEFAULT 0 COMMENT "true if this nic has additional network 
associations in nic_network_map"');
diff --git 
a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java
 
b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java
index 8e8ae314312..71c862ccfc2 100644
--- 
a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java
+++ 
b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java
@@ -23,6 +23,8 @@ import static com.cloud.host.Host.HOST_VDDK_LIB_DIR;
 import static com.cloud.host.Host.HOST_VDDK_SUPPORT;
 import static com.cloud.host.Host.HOST_VDDK_VERSION;
 import static com.cloud.host.Host.HOST_VIRTV2V_VERSION;
+import static com.cloud.host.Host.HOST_VLAN_FILTERING_ENABLED;
+import static com.cloud.host.Host.HOST_VLAN_TRUNK_XML_SUPPORTED;
 import static com.cloud.host.Host.HOST_VOLUME_ENCRYPTION;
 import static org.apache.cloudstack.utils.linux.KVMHostInfo.isHostS390x;
 
@@ -352,6 +354,10 @@ public class LibvirtComputingResource extends 
ServerResourceBase implements Serv
      * It is supported since Libvirt 0.9.0
      */
     private static final int MIN_LIBVIRT_VERSION_FOR_GUEST_CPU_TUNE = 9000;
+    /**
+     * Libvirt supports multi-tag trunk mode (&lt;vlan trunk='yes'&gt;) on a 
standard Linux bridge since 11.0.0.
+     */
+    private static final long MIN_LIBVIRT_VERSION_FOR_VLAN_TRUNK = 11000000;
     /**
      * Constant that defines ARM64 (aarch64) guest architectures.
      */
@@ -4415,6 +4421,8 @@ public class LibvirtComputingResource extends 
ServerResourceBase implements Serv
         cmd.getHostDetails().put(HOST_INSTANCE_CONVERSION, 
String.valueOf(instanceConversionSupported));
         cmd.getHostDetails().put(HOST_VDDK_SUPPORT, 
String.valueOf(hostSupportsVddk()));
         cmd.getHostDetails().put(HOST_CDROM_MAX_COUNT, 
String.valueOf(LibvirtVMDef.MAX_CDROMS_PER_VM));
+        cmd.getHostDetails().put(HOST_VLAN_FILTERING_ENABLED, 
String.valueOf(hostSupportsVlanFiltering()));
+        cmd.getHostDetails().put(HOST_VLAN_TRUNK_XML_SUPPORTED, 
String.valueOf(hostSupportsVlanTrunkXml()));
         if (StringUtils.isNotBlank(vddkLibDir)) {
             cmd.getHostDetails().put(HOST_VDDK_LIB_DIR, vddkLibDir);
         }
@@ -6236,6 +6244,39 @@ public class LibvirtComputingResource extends 
ServerResourceBase implements Serv
         return exitValue == 0;
     }
 
+    /**
+     * Static capability: whether this host's libvirt understands multi-tag 
trunk vlan XML
+     * (&lt;vlan trunk='yes'&gt;) on a standard Linux bridge interface.
+     */
+    public boolean hostSupportsVlanTrunkXml() {
+        return hypervisorLibvirtVersion >= MIN_LIBVIRT_VERSION_FOR_VLAN_TRUNK;
+    }
+
+    /**
+     * Live state: whether vlan_filtering is currently enabled on this host's 
guest bridge.
+     * A vlan_filtering=0 bridge floods every tagged frame to every port 
regardless of tag,
+     * so this must be true before any multi-VLAN trunk nic can be placed on 
this host.
+     */
+    public boolean hostSupportsVlanFiltering() {
+        return isBridgeVlanFilteringEnabled(guestBridgeName);
+    }
+
+    protected boolean isBridgeVlanFilteringEnabled(String bridgeName) {
+        if (StringUtils.isBlank(bridgeName)) {
+            return false;
+        }
+        File vlanFilteringFile = new File("/sys/class/net/" + bridgeName + 
"/bridge/vlan_filtering");
+        if (!vlanFilteringFile.exists()) {
+            return false;
+        }
+        try {
+            return 
"1".equals(FileUtils.readFileToString(vlanFilteringFile).trim());
+        } catch (IOException e) {
+            LOGGER.warn("Failed to read vlan_filtering state for bridge " + 
bridgeName, e);
+            return false;
+        }
+    }
+
     public boolean hostSupportsVddk() {
         return hostSupportsVddk(null);
     }
diff --git 
a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtReadyCommandWrapper.java
 
b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtReadyCommandWrapper.java
index 5a7d6d2c203..8af4e5f95d0 100644
--- 
a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtReadyCommandWrapper.java
+++ 
b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtReadyCommandWrapper.java
@@ -59,6 +59,9 @@ public final class LibvirtReadyCommandWrapper extends 
CommandWrapper<ReadyComman
             hostDetails.put(Host.HOST_OVFTOOL_VERSION, 
libvirtComputingResource.getHostOvfToolVersion());
         }
 
+        hostDetails.put(Host.HOST_VLAN_FILTERING_ENABLED, 
Boolean.toString(libvirtComputingResource.hostSupportsVlanFiltering()));
+        hostDetails.put(Host.HOST_VLAN_TRUNK_XML_SUPPORTED, 
Boolean.toString(libvirtComputingResource.hostSupportsVlanTrunkXml()));
+
         return new ReadyAnswer(command, hostDetails);
     }
 
diff --git a/server/src/main/java/com/cloud/api/query/dao/HostJoinDaoImpl.java 
b/server/src/main/java/com/cloud/api/query/dao/HostJoinDaoImpl.java
index 2e4025ca001..f942a38746c 100644
--- a/server/src/main/java/com/cloud/api/query/dao/HostJoinDaoImpl.java
+++ b/server/src/main/java/com/cloud/api/query/dao/HostJoinDaoImpl.java
@@ -283,6 +283,8 @@ public class HostJoinDaoImpl extends 
GenericDaoBase<HostJoinVO, Long> implements
                     } else {
                         hostResponse.setUefiCapability(new Boolean(false));
                     }
+                    
hostResponse.setVlanFilteringEnabled(Boolean.parseBoolean((String) 
hostDetails.get(Host.HOST_VLAN_FILTERING_ENABLED)));
+                    
hostResponse.setVlanTrunkXmlSupported(Boolean.parseBoolean((String) 
hostDetails.get(Host.HOST_VLAN_TRUNK_XML_SUPPORTED)));
                 }
                 if (details.contains(HostDetails.all) &&
                         Arrays.asList(Hypervisor.HypervisorType.KVM,

Reply via email to