Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
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
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;
}
}
8 changes: 8 additions & 0 deletions api/src/main/java/com/cloud/event/EventTypes.java
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,11 @@ public class EventTypes {
public static final String EVENT_NIC_DETAIL_ADD = "NIC.DETAIL.ADD";
public static final String EVENT_NIC_DETAIL_UPDATE = "NIC.DETAIL.UPDATE";
public static final String EVENT_NIC_DETAIL_REMOVE = "NIC.DETAIL.REMOVE";
public static final String EVENT_NIC_NETWORK_ASSOCIATE = "NIC.NETWORK.ASSOCIATE";
public static final String EVENT_NIC_NETWORK_DISASSOCIATE = "NIC.NETWORK.DISASSOCIATE";
public static final String EVENT_NIC_PRIMARY_NETWORK_CHANGE = "NIC.PRIMARY.NETWORK.CHANGE";
// internal-only signal consumed by the usage job - never user-facing, not an audit event
public static final String EVENT_NIC_NETWORK_ID_BACKFILL = "NIC.NETWORK.ID.BACKFILL";

// Load Balancers
public static final String EVENT_ASSIGN_TO_LOAD_BALANCER_RULE = "LB.ASSIGN.TO.RULE";
Expand Down Expand Up @@ -980,6 +985,9 @@ public class EventTypes {

// Nic Events
entityEventDetails.put(EVENT_NIC_CREATE, Nic.class);
entityEventDetails.put(EVENT_NIC_NETWORK_ASSOCIATE, Nic.class);
entityEventDetails.put(EVENT_NIC_NETWORK_DISASSOCIATE, Nic.class);
entityEventDetails.put(EVENT_NIC_PRIMARY_NETWORK_CHANGE, Nic.class);

// Load Balancers
entityEventDetails.put(EVENT_ASSIGN_TO_LOAD_BALANCER_RULE, FirewallRule.class);
Expand Down
2 changes: 2 additions & 0 deletions api/src/main/java/com/cloud/host/Host.java
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ public static String[] toStrings(Host.Type... types) {
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";

Expand Down
8 changes: 8 additions & 0 deletions api/src/main/java/com/cloud/network/NetworkModel.java
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ public interface NetworkModel {
String CLOUD_DOMAIN_FILE = "cloud-domain";
String CLOUD_DOMAIN_ID_FILE = "cloud-domain-id";
String CLOUD_NAME_FILE = "cloud-name";
String NIC_VLAN_MAPPING_FILE = "nic-vlan-mapping";
int CONFIGDATA_DIR = 0;
int CONFIGDATA_FILE = 1;
int CONFIGDATA_CONTENT = 2;
Expand Down Expand Up @@ -137,6 +138,13 @@ public interface NetworkModel {

Nic getNicInNetwork(long vmId, long networkId);

/**
* Resolves the nic and guest ip(s) a VM answers on for a given network - the nic's own primary network match
* when one exists, or a trunk nic's association (from nic_network_map) otherwise. Returns null if the VM has
* neither a primary nor an associated nic in that network.
*/
Pair<Nic, Network.IpAddresses> getNicAndIpInNetwork(long vmId, long networkId);

List<? extends Nic> getNicsForTraffic(long vmId, TrafficType type);

Network getDefaultNetworkForVm(long vmId);
Expand Down
29 changes: 29 additions & 0 deletions api/src/main/java/com/cloud/network/NetworkService.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@
import org.apache.cloudstack.api.command.user.network.ResetNetworkPermissionsCmd;
import org.apache.cloudstack.api.command.user.network.RestartNetworkCmd;
import org.apache.cloudstack.api.command.user.network.UpdateNetworkCmd;
import org.apache.cloudstack.api.command.admin.network.AssociateNetworkToNicCmd;
import org.apache.cloudstack.api.command.admin.network.ChangeNicPrimaryNetworkCmd;
import org.apache.cloudstack.api.command.admin.network.DisassociateNetworkFromNicCmd;
import org.apache.cloudstack.api.command.user.vm.ListNicsCmd;
import org.apache.cloudstack.api.response.AcquirePodIpCmdResponse;
import org.apache.cloudstack.framework.config.ConfigKey;
Expand Down Expand Up @@ -249,6 +252,32 @@ Network createPrivateNetwork(String networkName, String displayText, long physic

List<? extends NicSecondaryIp> listVmNicSecondaryIps(ListNicsCmd listNicsCmd);

/**
* Associates one or more additional networks with an existing nic, delivering them as a multi-VLAN trunk
*/
Nic associateNetworkToNic(AssociateNetworkToNicCmd cmd)
throws ConcurrentOperationException, ResourceUnavailableException, InsufficientCapacityException, InsufficientAddressCapacityException;

/**
* Disassociates a network from an existing nic, shrinking its multi-VLAN trunk. The primary network cannot be disassociated.
*/
Nic disassociateNetworkFromNic(DisassociateNetworkFromNicCmd cmd) throws ResourceUnavailableException;

/**
* Changes a multi-VLAN trunk nic's primary network to one of its already-associated networks. Only allowed while
* the owning Instance is stopped, since the nic is rebuilt fresh on its next start.
*/
Nic changeNicPrimaryNetwork(ChangeNicPrimaryNetworkCmd cmd);

/**
* Associates additional networks with a nic that was just created (e.g. during VM deploy), before the owning
* Instance has ever started. No live agent update is issued - the nic's first plug already reads these
* associations fresh. requestedIps (nullable) optionally requests a specific ip4/ip6 address per associated
* network id - a network with no entry (or a null map) auto-allocates, same as today.
*/
void associateNetworksToNic(Nic nic, List<Long> networkIds, Map<Long, IpAddresses> requestedIps)
throws ConcurrentOperationException, ResourceUnavailableException, InsufficientCapacityException, InsufficientAddressCapacityException;

AcquirePodIpCmdResponse allocatePodIp(Account account, String zoneId, String podId) throws ResourceAllocationException, ConcurrentOperationException;

boolean releasePodIp(ReleasePodIpCmdByAdmin ip) throws CloudRuntimeException;
Expand Down
2 changes: 2 additions & 0 deletions api/src/main/java/com/cloud/vm/Nic.java
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,8 @@ public enum ReservationStrategy {

boolean getSecondaryIp();

boolean getMultiNetwork();

//
// IPv4
//
Expand Down
5 changes: 5 additions & 0 deletions api/src/main/java/org/apache/cloudstack/api/ApiConstants.java
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -420,6 +422,7 @@ public class ApiConstants {
public static final String NIC = "nic";
public static final String NICS = "nics";
public static final String NIC_NETWORK_LIST = "nicnetworklist";
public static final String NIC_NETWORKS_LIST = "nicnetworkslist";
public static final String NIC_IP_ADDRESS_LIST = "nicipaddresslist";
public static final String NIC_MULTIQUEUE_NUMBER = "nicmultiqueuenumber";
public static final String NIC_PACKED_VIRTQUEUES_ENABLED = "nicpackedvirtqueuesenabled";
Expand Down Expand Up @@ -865,6 +868,8 @@ public class ApiConstants {
public static final String ASSOCIATED_NETWORK = "associatednetwork";
public static final String ASSOCIATED_NETWORK_ID = "associatednetworkid";
public static final String ASSOCIATED_NETWORK_NAME = "associatednetworkname";
public static final String ASSOCIATED_NETWORKS = "associatednetworks";
public static final String TRUNKED = "trunked";
public static final String ASSOCIATED_VPC_ID = "associatedvpcid";
public static final String ASSOCIATED_VPC_NAME = "associatedvpcname";
public static final String SOURCE_NAT_SUPPORTED = "sourcenatsupported";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
// 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 org.apache.cloudstack.api.command.admin.network;

import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiCommandResourceType;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.ApiErrorCode;
import org.apache.cloudstack.api.BaseAsyncCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.response.NetworkResponse;
import org.apache.cloudstack.api.response.NicResponse;
import org.apache.cloudstack.context.CallContext;
import org.apache.commons.lang3.StringUtils;

import com.cloud.event.EventTypes;
import com.cloud.exception.ConcurrentOperationException;
import com.cloud.exception.InsufficientAddressCapacityException;
import com.cloud.exception.InsufficientCapacityException;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.exception.ResourceUnavailableException;
import com.cloud.network.Network;
import com.cloud.user.Account;
import com.cloud.vm.Nic;
import com.cloud.vm.VirtualMachine;

@APICommand(name = "associateNetworkToNic", description = "Associates one or more additional networks with an existing NIC, delivering them as a multi-VLAN trunk",
responseObject = NicResponse.class, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, since = "24.0.0")
public class AssociateNetworkToNicCmd extends BaseAsyncCmd {
private static final String s_name = "associatenetworktonicresponse";

/////////////////////////////////////////////////////
//////////////// API parameters /////////////////////
/////////////////////////////////////////////////////

@Parameter(name = ApiConstants.NIC_ID, type = CommandType.UUID, entityType = NicResponse.class, required = true,
description = "the ID of the NIC to associate additional networks with")
private Long nicId;

@Parameter(name = ApiConstants.NETWORK_IDS, type = CommandType.LIST, collectionType = CommandType.UUID, entityType = NetworkResponse.class, required = true,
description = "the IDs of the additional networks to associate with the NIC")
private List<Long> networkIds;

@Parameter(name = ApiConstants.IP_ADDRESSES, type = CommandType.MAP, required = false,
description = "optional per-network IP addresses, keyed by network. Example: "
+ "ipaddresses[0].networkid=UUID&ipaddresses[0].ipaddress=10.0.0.5&ipaddresses[0].ip6address=fd00::5")
private Map ipAddresses;

/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////

public Long getNicId() {
return nicId;
}

public List<Long> getNetworkIds() {
return networkIds;
}

public Map<Long, Network.IpAddresses> getIpAddressesMap() {
Map<Long, Network.IpAddresses> result = new HashMap<>();
if (ipAddresses == null || ipAddresses.isEmpty()) {
return result;
}
Collection<Map<String, String>> entries = ipAddresses.values();
for (Map<String, String> entry : entries) {
String networkUuid = entry.get(ApiConstants.NETWORK_ID);
if (networkUuid == null) {
throw new InvalidParameterValueException(String.format("Each ipaddresses entry must specify %s", ApiConstants.NETWORK_ID));
}
Network network = _entityMgr.findByUuid(Network.class, networkUuid);
if (network == null) {
throw new InvalidParameterValueException(String.format("Unable to find network with id: %s", networkUuid));
}
String ip4Address = entry.get(ApiConstants.IP_ADDRESS);
String ip6Address = entry.get(ApiConstants.IP6_ADDRESS);
result.put(network.getId(), new Network.IpAddresses(ip4Address, ip6Address));
}
return result;
}

/////////////////////////////////////////////////////
/////////////// API Implementation ///////////////////
/////////////////////////////////////////////////////

@Override
public String getCommandName() {
return s_name;
}

@Override
public String getEventType() {
return EventTypes.EVENT_NIC_NETWORK_ASSOCIATE;
}

@Override
public String getEventDescription() {
return "Associating networks " + StringUtils.join(getNetworkIds(), ",") + " with nic " + getResourceUuid(ApiConstants.NIC_ID);
}

private Nic getNic() {
Nic nic = _entityMgr.findById(Nic.class, nicId);
if (nic == null) {
throw new InvalidParameterValueException("Can't find NIC for id specified");
}
return nic;
}

@Override
public long getEntityOwnerId() {
VirtualMachine vm = _entityMgr.findById(VirtualMachine.class, getNic().getInstanceId());
if (vm == null) {
return Account.ACCOUNT_ID_SYSTEM;
}
return vm.getAccountId();
}

@Override
public ApiCommandResourceType getApiResourceType() {
return ApiCommandResourceType.VirtualMachine;
}

@Override
public Long getApiResourceId() {
return getNic().getInstanceId();
}

@Override
public void execute() throws ConcurrentOperationException, ResourceUnavailableException, InsufficientCapacityException, InsufficientAddressCapacityException {
CallContext.current().setEventDetails("Nic ID: " + getResourceUuid(ApiConstants.NIC_ID));
Nic result = _networkService.associateNetworkToNic(this);
if (result == null) {
throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to associate network to NIC. Refer to server logs for details.");
}
NicResponse response = _responseGenerator.createNicResponse(result);
response.setResponseName(getCommandName());
setResponseObject(response);
}
}
Loading
Loading