Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
e45ea95
engine-schema: upgrade path for 24.0.0
shwstppr Sep 14, 2026
c2ebb3a
Support multi-VLAN trunk NICs
Pearl1594 Sep 14, 2026
219f9cb
Merge branch 'main' of github.com:apache/cloudstack into support-mult…
Pearl1594 Sep 14, 2026
eb97acf
deliver multi-VLAN trunk nics via libvirt vlan XML and bridge fallback
Pearl1594 Sep 16, 2026
d143862
Fix nic_network_map unique constraint blocking re-association
Pearl1594 Sep 28, 2026
a7b6a8e
Deliver multi-VLAN trunk NICs on KVM via libvirt vlan XML and bridge …
Pearl1594 Sep 28, 2026
bcb89ab
Bill usage for a trunk nic's associated networks
Pearl1594 Sep 28, 2026
3d27d89
Expose trunk nic and associated-network info in NicResponse
Pearl1594 Sep 28, 2026
dd4ee2a
Add ApiConstants for NicResponse trunk fields
Pearl1594 Sep 28, 2026
c16a58d
Add multi-VLAN trunk NIC association service
Pearl1594 Sep 28, 2026
88585ed
Add EventTypes constants for the association service commit
Pearl1594 Sep 28, 2026
df74bd9
Support multi-VLAN trunk NICs at VM deploy time
Pearl1594 Sep 28, 2026
3a7519d
Fix static NAT lookup in disassociate guard
Pearl1594 Sep 28, 2026
68d7771
Fix VNF management nic trunkable via live associate API
Pearl1594 Sep 28, 2026
485ad26
Fix stale nic fields after changing its primary network
Pearl1594 Sep 28, 2026
721bae1
Clear multiNetwork flag when a nic's last trunk association is removed
Pearl1594 Sep 29, 2026
1a3ddfb
Push DHCP entries to a trunk nic's associated networks
Pearl1594 Sep 29, 2026
5088e0b
Add KVM live migration support for multi-VLAN trunk NICs (Phase 7)
Pearl1594 Oct 2, 2026
01729b9
Merge branch 'main' of github.com:apache/cloudstack into support-mult…
Pearl1594 Oct 2, 2026
c9b98ea
fix test
Pearl1594 Oct 2, 2026
5f95946
Fix CI build failures from the multi-VLAN trunk NIC changes
Pearl1594 Oct 2, 2026
b4c18d6
Fix NullPointerException in LibvirtComputingResourceTest.testMigrateC…
Pearl1594 Oct 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
deliver multi-VLAN trunk nics via libvirt vlan XML and bridge fallback
  • Loading branch information
Pearl1594 committed Sep 16, 2026
commit eb97acf2fbd775f5d4269220ff4710d8a06c0be5
20 changes: 20 additions & 0 deletions api/src/main/java/com/cloud/agent/api/to/NicTO.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ public class NicTO extends NetworkTO {

String networkSegmentName;

boolean trunkVlan;
List<NetworkTO> associatedNetworks;

public NicTO() {
super();
}
Expand Down Expand Up @@ -163,4 +166,21 @@ public boolean isEnabled() {
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}

// trunkVlan/associatedNetworks carry the additional networks for a multi-VLAN trunk nic; unset for ordinary nics
public boolean isTrunkVlan() {
return trunkVlan;
}

public void setTrunkVlan(boolean trunkVlan) {
this.trunkVlan = trunkVlan;
}

public List<NetworkTO> getAssociatedNetworks() {
return associatedNetworks;
}

public void setAssociatedNetworks(List<NetworkTO> associatedNetworks) {
this.associatedNetworks = associatedNetworks;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,13 @@
package com.cloud.hypervisor.kvm.resource;

import java.io.File;
import java.net.URI;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

Expand All @@ -33,6 +37,7 @@
import org.apache.commons.lang3.StringUtils;
import org.libvirt.LibvirtException;

import com.cloud.agent.api.to.NetworkTO;
import com.cloud.agent.api.to.NicTO;
import com.cloud.agent.properties.AgentProperties;
import com.cloud.agent.properties.AgentPropertiesFileHandler;
Expand All @@ -43,6 +48,8 @@

public class BridgeVifDriver extends VifDriverBase {

private static final String GUEST_UPLINK_TRUNK_VLAN_RANGE = "2-4094";

private int _timeout;

private final Object _vnetBridgeMonitor = new Object();
Expand All @@ -51,6 +58,7 @@
private String _macIpScriptPath;
private String _controlCidr = NetUtils.getLinkLocalCIDR();
private Long libvirtVersion;
private final Set<String> uplinkVlanTrunkEnsuredBridges = ConcurrentHashMap.newKeySet();

private static boolean isVxlanOrNetris(String protocol) {
return protocol.equals(Networks.BroadcastDomainType.Vxlan.scheme()) || protocol.equals(Networks.BroadcastDomainType.Netris.scheme());
Expand Down Expand Up @@ -199,6 +207,102 @@
return vNetId != null && protocol != null && !vNetId.equalsIgnoreCase("untagged");
}

protected void plugTrunkVlanNic(LibvirtVMDef.InterfaceDef intf, NicTO nic, String trafficLabel, String guestOsType, String nicAdapter,
Integer networkRateKBps) throws InternalErrorException {
if (nic.getBroadcastType() != Networks.BroadcastDomainType.Vlan) {
throw new InternalErrorException("Multi-VLAN trunk nics are only supported on VLAN-isolated guest networks");
}
if (!_libvirtComputingResource.hostSupportsVlanFiltering()) {
throw new InternalErrorException("vlan_filtering is not enabled on this host's guest bridge; "
+ "this host cannot accept a multi-VLAN trunk nic");
}

String brName = trafficLabel != null && !trafficLabel.isEmpty() ? trafficLabel : _bridges.get("guest");

Check failure on line 220 in plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/BridgeVifDriver.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal "guest" 3 times.

See more on https://sonarcloud.io/project/issues?id=apache_cloudstack&issues=AaCq822Lt_SkqufYqmdI&open=AaCq822Lt_SkqufYqmdI&pullRequest=14166

ensureUplinkAllowsAllVlans(brName);

List<Integer> vlanTags = collectTrunkVlanTags(nic);

logger.debug("plugging trunk nic " + nic.getMac() + " onto guest bridge " + brName + " with vlan tags " + vlanTags);

Check warning on line 226 in plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/BridgeVifDriver.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use the built-in formatting to construct this argument.

See more on https://sonarcloud.io/project/issues?id=apache_cloudstack&issues=AaCq822Lt_SkqufYqmdH&open=AaCq822Lt_SkqufYqmdH&pullRequest=14166

Check warning on line 226 in plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/BridgeVifDriver.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Format specifiers should be used instead of string concatenation.

See more on https://sonarcloud.io/project/issues?id=apache_cloudstack&issues=AaCq822Lt_SkqufYqmdJ&open=AaCq822Lt_SkqufYqmdJ&pullRequest=14166
intf.defBridgeNet(brName, null, nic.getMac(), getGuestNicModel(guestOsType, nicAdapter), networkRateKBps);

if (_libvirtComputingResource.hostSupportsVlanTrunkXml()) {
intf.setTrunkVlanTags(vlanTags);
}
// else: older libvirt can't express trunk membership; ensureVlanTrunkMembership() applies it manually once the tap exists
}

private List<Integer> collectTrunkVlanTags(NicTO nic) throws InternalErrorException {
Set<Integer> vlanTags = new LinkedHashSet<>();
vlanTags.add(parseVlanTag(nic.getBroadcastUri(), "primary network of nic " + nic.getMac()));
if (nic.getAssociatedNetworks() != null) {
for (NetworkTO associatedNetwork : nic.getAssociatedNetworks()) {
if (associatedNetwork.getBroadcastType() != Networks.BroadcastDomainType.Vlan) {
throw new InternalErrorException("Multi-VLAN trunk nics only support VLAN-isolated associated networks");
}
vlanTags.add(parseVlanTag(associatedNetwork.getBroadcastUri(), "associated network " + associatedNetwork.getUuid()));
}
}
return new ArrayList<>(vlanTags);
}

private Integer parseVlanTag(URI broadcastUri, String description) throws InternalErrorException {
String vlanValue = broadcastUri == null ? null : Networks.BroadcastDomainType.getValue(broadcastUri);
if (StringUtils.isBlank(vlanValue)) {
throw new InternalErrorException("Cannot determine VLAN for " + description
+ ": no VLAN has been assigned yet (is the network implemented?). Refusing to plug this multi-VLAN trunk nic.");
}
try {
return Integer.valueOf(vlanValue);
} catch (NumberFormatException e) {
throw new InternalErrorException("Invalid VLAN value '" + vlanValue + "' for " + description);
}
}

private void ensureUplinkAllowsAllVlans(String brName) throws InternalErrorException {
if (uplinkVlanTrunkEnsuredBridges.contains(brName)) {
return;
}
synchronized (_vnetBridgeMonitor) {
if (uplinkVlanTrunkEnsuredBridges.contains(brName)) {
return;
}
String uplinkPif = _pifs.get(brName);
if (StringUtils.isBlank(uplinkPif)) {
throw new InternalErrorException("Cannot determine the uplink interface for guest bridge " + brName
+ "; refusing to plug a multi-VLAN trunk nic");
}
runBridgeVlanCommand("add", uplinkPif, GUEST_UPLINK_TRUNK_VLAN_RANGE);
uplinkVlanTrunkEnsuredBridges.add(brName);
}
}

@Override
public void ensureVlanTrunkMembership(LibvirtVMDef.InterfaceDef iface, NicTO nic) throws InternalErrorException {
if (!nic.isTrunkVlan() || _libvirtComputingResource.hostSupportsVlanTrunkXml()) {
return;
}
String tapName = iface.getDevName();
if (StringUtils.isBlank(tapName)) {
throw new InternalErrorException("Cannot apply manual VLAN trunk membership: tap device name unknown for nic " + nic.getMac());
}
for (Integer vlanTag : collectTrunkVlanTags(nic)) {
runBridgeVlanCommand("add", tapName, String.valueOf(vlanTag));
}
}

protected void runBridgeVlanCommand(String operation, String dev, String vid) throws InternalErrorException {
final Script command = new Script("bridge", _timeout, logger);
command.add("vlan");
command.add(operation);
command.add("dev", dev);
command.add("vid", vid);
final String result = command.execute();
if (result != null) {
throw new InternalErrorException("Failed to " + operation + " VLAN " + vid + " membership on " + dev + ": " + result);
}
}

protected String createStorageVnetBridgeIfNeeded(NicTO nic, String trafficLabel,
String storageBrName) throws InternalErrorException {
if (nic.getBroadcastUri() == null) {
Expand Down Expand Up @@ -248,7 +352,9 @@
}

if (nic.getType() == Networks.TrafficType.Guest) {
if (isBroadcastTypeVlanOrVxlan(nic) && isValidProtocolAndVnetId(vNetId, protocol)) {
if (nic.isTrunkVlan()) {
plugTrunkVlanNic(intf, nic, trafficLabel, guestOsType, nicAdapter, networkRateKBps);
} else if (isBroadcastTypeVlanOrVxlan(nic) && isValidProtocolAndVnetId(vNetId, protocol)) {
if (trafficLabel != null && !trafficLabel.isEmpty()) {
logger.debug("creating a vNet dev and bridge for guest traffic per traffic label " + trafficLabel);
String brName = createVnetBr(vNetId, trafficLabel, protocol);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import com.cloud.cpu.CPU;
import org.apache.cloudstack.api.ApiConstants.IoDriverPolicy;
import org.apache.cloudstack.utils.qemu.QemuObject;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringEscapeUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.Logger;
Expand Down Expand Up @@ -1602,6 +1603,8 @@
private String _virtualPortType;
private String _virtualPortInterfaceId;
private int _vlanTag = -1;
private boolean _vlanTrunk = false;
private List<Integer> _vlanTrunkTags;
private boolean _pxeDisable = false;
private boolean _linkStateUp = true;
private Integer _slot;
Expand All @@ -1611,8 +1614,8 @@
private String _interfaceMode;
private String _userIp4Network;
private Integer _userIp4Prefix;
private Integer _multiQueueNumber;

Check warning on line 1617 in plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtVMDef.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename this field "_vlanTrunk" to match the regular expression '^[a-z][a-zA-Z0-9]*$'.

See more on https://sonarcloud.io/project/issues?id=apache_cloudstack&issues=AaCq828-t_SkqufYqmdK&open=AaCq828-t_SkqufYqmdK&pullRequest=14166
private Boolean _packedVirtQueues;

Check warning on line 1618 in plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtVMDef.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename this field "_vlanTrunkTags" to match the regular expression '^[a-z][a-zA-Z0-9]*$'.

See more on https://sonarcloud.io/project/issues?id=apache_cloudstack&issues=AaCq828-t_SkqufYqmdL&open=AaCq828-t_SkqufYqmdL&pullRequest=14166

public void defBridgeNet(String brName, String targetBrName, String macAddr, NicModel model) {
defBridgeNet(brName, targetBrName, macAddr, model, 0);
Expand Down Expand Up @@ -1762,6 +1765,19 @@
return _vlanTag;
}

public void setTrunkVlanTags(List<Integer> vlanTags) {
_vlanTrunk = true;
_vlanTrunkTags = vlanTags;
}

public List<Integer> getTrunkVlanTags() {
return _vlanTrunkTags;
}

public boolean isVlanTrunk() {
return _vlanTrunk;
}

public void setSlot(Integer slot) {
_slot = slot;
}
Expand Down Expand Up @@ -1865,7 +1881,13 @@
}
netBuilder.append("</virtualport>\n");
}
if (_vlanTag > 0 && _vlanTag < 4095) {
if (_vlanTrunk && CollectionUtils.isNotEmpty(_vlanTrunkTags)) {
netBuilder.append("<vlan trunk='yes'>\n");
for (Integer tag : _vlanTrunkTags) {
netBuilder.append("<tag id='" + tag + "'/>\n");
}
netBuilder.append("</vlan>");
} else if (_vlanTag > 0 && _vlanTag < 4095) {
netBuilder.append("<vlan trunk='no'>\n<tag id='" + _vlanTag + "'/>\n</vlan>");
}

Expand All @@ -1873,7 +1895,7 @@
netBuilder.append(_dpdkExtraLines);
}

if (_netType != GuestNetType.VHOSTUSER && _netType != GuestNetType.USER) {

Check warning on line 1898 in plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtVMDef.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use multiple calls to "append" instead of string concatenation.

See more on https://sonarcloud.io/project/issues?id=apache_cloudstack&issues=AaCq828-t_SkqufYqmdM&open=AaCq828-t_SkqufYqmdM&pullRequest=14166
netBuilder.append("<link state='" + (_linkStateUp ? "up" : "down") +"'/>\n");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,8 @@ public interface VifDriver {

void deleteBr(NicTO nic);

// applies manual VLAN trunk membership to a trunk nic's live tap on hosts whose libvirt can't do it via <vlan> XML; no-op otherwise
default void ensureVlanTrunkMembership(LibvirtVMDef.InterfaceDef iface, NicTO nic) throws InternalErrorException {
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import com.cloud.hypervisor.kvm.resource.VifDriver;
import com.cloud.resource.CommandWrapper;
import com.cloud.resource.ResourceWrapper;
import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.vm.VirtualMachine;
import org.libvirt.Connect;
import org.libvirt.Domain;
Expand Down Expand Up @@ -67,6 +68,15 @@
}
vm.attachDevice(interfaceDef.toString());

if (nic.isTrunkVlan()) {
try {

Check warning on line 72 in plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtPlugNicCommandWrapper.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Extract this nested try block into a separate method.

See more on https://sonarcloud.io/project/issues?id=apache_cloudstack&issues=AaCq82u1t_SkqufYqmdG&open=AaCq82u1t_SkqufYqmdG&pullRequest=14166
final InterfaceDef liveInterfaceDef = libvirtComputingResource.getInterface(conn, vmName, nic.getMac());
vifDriver.ensureVlanTrunkMembership(liveInterfaceDef, nic);
} catch (CloudRuntimeException e) {
throw new InternalErrorException("Failed to locate live tap for trunk nic " + nic.getMac() + ": " + e.getMessage());
}
}

// apply default network rules on new nic
if (vmType == VirtualMachine.Type.User && nic.isSecurityGroupEnabled()) {
final Long vmId = Long.valueOf(vmName.split("-")[2]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
import com.cloud.network.Networks.TrafficType;
import com.cloud.resource.CommandWrapper;
import com.cloud.resource.ResourceWrapper;
import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.vm.UserVmManager;
import com.cloud.vm.VirtualMachine;

Expand Down Expand Up @@ -96,6 +97,7 @@ public Answer execute(final StartCommand command, final LibvirtComputingResource
String vmFinalSpecification = performXmlTransformHook(vmInitialSpecification, libvirtComputingResource);
libvirtComputingResource.startVM(conn, vmName, vmFinalSpecification);
performAgentStartHook(vmName, libvirtComputingResource);
applyManualVlanTrunkMembership(conn, vmName, nics, libvirtComputingResource);

libvirtComputingResource.applyDefaultNetworkRules(conn, vmSpec, false);

Expand Down Expand Up @@ -179,6 +181,21 @@ public Answer execute(final StartCommand command, final LibvirtComputingResource
}
}

private void applyManualVlanTrunkMembership(Connect conn, String vmName, NicTO[] nics, LibvirtComputingResource libvirtComputingResource)
throws InternalErrorException {
for (NicTO nic : nics) {
if (!nic.isTrunkVlan()) {
continue;
}
try {
LibvirtVMDef.InterfaceDef liveInterface = libvirtComputingResource.getInterface(conn, vmName, nic.getMac());
libvirtComputingResource.getVifDriver(nic.getType(), nic.getName()).ensureVlanTrunkMembership(liveInterface, nic);
} catch (CloudRuntimeException e) {
throw new InternalErrorException("Failed to locate live tap for trunk nic " + nic.getMac() + ": " + e.getMessage());
}
}
}

private void mountSecondaryStoragesIfNeeded(StartCommand command, LibvirtComputingResource libvirtComputingResource, List<KVMStoragePool> secondaryStorages) {
if (CollectionUtils.isNotEmpty(command.getSecondaryStorages())) {
for (String secondaryStorageUrl : command.getSecondaryStorages()) {
Expand Down
Loading
Loading