diff --git a/api/src/main/java/com/cloud/agent/api/to/NicTO.java b/api/src/main/java/com/cloud/agent/api/to/NicTO.java index 2ed7d9f9a201..663a039ab100 100644 --- a/api/src/main/java/com/cloud/agent/api/to/NicTO.java +++ b/api/src/main/java/com/cloud/agent/api/to/NicTO.java @@ -37,6 +37,9 @@ public class NicTO extends NetworkTO { String networkSegmentName; + boolean trunkVlan; + List associatedNetworks; + public NicTO() { super(); } @@ -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 getAssociatedNetworks() { + return associatedNetworks; + } + + public void setAssociatedNetworks(List associatedNetworks) { + this.associatedNetworks = associatedNetworks; + } } diff --git a/api/src/main/java/com/cloud/event/EventTypes.java b/api/src/main/java/com/cloud/event/EventTypes.java index f7d13343d469..b2848abafa66 100644 --- a/api/src/main/java/com/cloud/event/EventTypes.java +++ b/api/src/main/java/com/cloud/event/EventTypes.java @@ -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"; @@ -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); diff --git a/api/src/main/java/com/cloud/host/Host.java b/api/src/main/java/com/cloud/host/Host.java index 79bec7a5871a..be8cca1bfe45 100644 --- a/api/src/main/java/com/cloud/host/Host.java +++ b/api/src/main/java/com/cloud/host/Host.java @@ -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"; diff --git a/api/src/main/java/com/cloud/network/NetworkModel.java b/api/src/main/java/com/cloud/network/NetworkModel.java index 7e1a07ebeb69..410d5da34ead 100644 --- a/api/src/main/java/com/cloud/network/NetworkModel.java +++ b/api/src/main/java/com/cloud/network/NetworkModel.java @@ -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; @@ -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 getNicAndIpInNetwork(long vmId, long networkId); + List getNicsForTraffic(long vmId, TrafficType type); Network getDefaultNetworkForVm(long vmId); diff --git a/api/src/main/java/com/cloud/network/NetworkService.java b/api/src/main/java/com/cloud/network/NetworkService.java index c32bb711c0f2..64342c6104fb 100644 --- a/api/src/main/java/com/cloud/network/NetworkService.java +++ b/api/src/main/java/com/cloud/network/NetworkService.java @@ -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; @@ -249,6 +252,32 @@ Network createPrivateNetwork(String networkName, String displayText, long physic List 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 networkIds, Map requestedIps) + throws ConcurrentOperationException, ResourceUnavailableException, InsufficientCapacityException, InsufficientAddressCapacityException; + AcquirePodIpCmdResponse allocatePodIp(Account account, String zoneId, String podId) throws ResourceAllocationException, ConcurrentOperationException; boolean releasePodIp(ReleasePodIpCmdByAdmin ip) throws CloudRuntimeException; diff --git a/api/src/main/java/com/cloud/server/ManagementService.java b/api/src/main/java/com/cloud/server/ManagementService.java index bcca229c06bc..029882f95ddf 100644 --- a/api/src/main/java/com/cloud/server/ManagementService.java +++ b/api/src/main/java/com/cloud/server/ManagementService.java @@ -472,6 +472,13 @@ public interface ManagementService { Ternary, Integer>, List, Map> listHostsForMigrationOfVM(VirtualMachine vm, Long startIndex, Long pageSize, String keyword, List vmList); + /** + * If vm has a multi-VLAN trunk NIC and host is not ready for it (VLAN filtering not enabled), returns a + * human-readable reason; null otherwise (including when vm has no such NIC, in which case host readiness + * for this concern is irrelevant). + */ + String getMultiNetworkNicUnsuitableReason(VirtualMachine vm, Host host); + /** * Apply affinity group constraints and other exclusion rules for VM migration. * This is a helper method that can be used independently for per-iteration affinity checks in DRS. diff --git a/api/src/main/java/com/cloud/vm/Nic.java b/api/src/main/java/com/cloud/vm/Nic.java index 3722e5769c92..71612cd355f1 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 enum ReservationStrategy { 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 f74c46161180..c822f80c54f1 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"; @@ -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"; @@ -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"; diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/host/FindHostsForMigrationCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/host/FindHostsForMigrationCmd.java index 4d6ef7419616..a56880de66c0 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/host/FindHostsForMigrationCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/host/FindHostsForMigrationCmd.java @@ -32,6 +32,7 @@ import com.cloud.host.Host; import com.cloud.utils.Pair; import com.cloud.utils.Ternary; +import com.cloud.vm.VirtualMachine; @APICommand(name = "findHostsForMigration", description = "Find hosts suitable for migrating an Instance.", responseObject = HostForMigrationResponse.class, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false) @@ -73,6 +74,8 @@ public void execute() { List hostsWithCapacity = hostsForMigration.second(); hostsRequiringStorageMotion = hostsForMigration.third(); + VirtualMachine vm = _entityMgr.findById(VirtualMachine.class, getVirtualMachineId()); + response = new ListResponse(); List hostResponses = new ArrayList(); for (Host host : result.first()) { @@ -90,6 +93,10 @@ public void execute() { hostResponse.setRequiresStorageMotion(false); } + if (!suitableForMigration && vm != null) { + hostResponse.setUnsuitableReason(_mgr.getMultiNetworkNicUnsuitableReason(vm, host)); + } + hostResponse.setObjectName("host"); hostResponses.add(hostResponse); } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/AssociateNetworkToNicCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/AssociateNetworkToNicCmd.java new file mode 100644 index 000000000000..2fa45197ea50 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/AssociateNetworkToNicCmd.java @@ -0,0 +1,162 @@ +// 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.acl.RoleType; +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", + authorized = {RoleType.Admin}) +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 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 getNetworkIds() { + return networkIds; + } + + public Map getIpAddressesMap() { + Map result = new HashMap<>(); + if (ipAddresses == null || ipAddresses.isEmpty()) { + return result; + } + Collection> entries = ipAddresses.values(); + for (Map 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); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/ChangeNicPrimaryNetworkCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/ChangeNicPrimaryNetworkCmd.java new file mode 100644 index 000000000000..93d48aa1dd2a --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/ChangeNicPrimaryNetworkCmd.java @@ -0,0 +1,126 @@ +// 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 org.apache.cloudstack.acl.RoleType; +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 com.cloud.event.EventTypes; +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.user.Account; +import com.cloud.vm.Nic; +import com.cloud.vm.VirtualMachine; + +@APICommand(name = "changeNicPrimaryNetwork", description = "Changes a multi-VLAN trunk NIC's primary (native/untagged) network to one of its already-associated networks. " + + "Only allowed while the owning Instance is stopped. The operator is responsible for confirming the guest will " + + "reacquire its address on the new primary network (e.g. via DHCP) after the next start - CloudStack cannot verify this from the host side.", + responseObject = NicResponse.class, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, since = "24.0.0", + authorized = {RoleType.Admin}) +public class ChangeNicPrimaryNetworkCmd extends BaseAsyncCmd { + private static final String s_name = "changenicprimarynetworkresponse"; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.NIC_ID, type = CommandType.UUID, entityType = NicResponse.class, required = true, + description = "the ID of the NIC whose primary network should be changed") + private Long nicId; + + @Parameter(name = ApiConstants.NETWORK_ID, type = CommandType.UUID, entityType = NetworkResponse.class, required = true, + description = "the ID of the network to make primary; it must already be associated with this NIC") + private Long networkId; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public Long getNicId() { + return nicId; + } + + public Long getNetworkId() { + return networkId; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation /////////////////// + ///////////////////////////////////////////////////// + + @Override + public String getCommandName() { + return s_name; + } + + @Override + public String getEventType() { + return EventTypes.EVENT_NIC_PRIMARY_NETWORK_CHANGE; + } + + @Override + public String getEventDescription() { + return "Changing nic " + getResourceUuid(ApiConstants.NIC_ID) + "'s primary network to " + getResourceUuid(ApiConstants.NETWORK_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() { + CallContext.current().setEventDetails("Nic ID: " + getResourceUuid(ApiConstants.NIC_ID)); + Nic result = _networkService.changeNicPrimaryNetwork(this); + if (result == null) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to change nic's primary network. Refer to server logs for details."); + } + NicResponse response = _responseGenerator.createNicResponse(result); + response.setResponseName(getCommandName()); + setResponseObject(response); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/DisassociateNetworkFromNicCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/DisassociateNetworkFromNicCmd.java new file mode 100644 index 000000000000..58b1450c032a --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/DisassociateNetworkFromNicCmd.java @@ -0,0 +1,125 @@ +// 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 org.apache.cloudstack.acl.RoleType; +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 com.cloud.event.EventTypes; +import com.cloud.exception.ResourceUnavailableException; +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.user.Account; +import com.cloud.vm.Nic; +import com.cloud.vm.VirtualMachine; + +@APICommand(name = "disassociateNetworkFromNic", description = "Disassociates a network from an existing NIC, shrinking its multi-VLAN trunk", + responseObject = NicResponse.class, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, since = "24.0.0", + authorized = {RoleType.Admin}) +public class DisassociateNetworkFromNicCmd extends BaseAsyncCmd { + private static final String s_name = "disassociatenetworkfromnicresponse"; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.NIC_ID, type = CommandType.UUID, entityType = NicResponse.class, required = true, + description = "the ID of the NIC to disassociate the network from") + private Long nicId; + + @Parameter(name = ApiConstants.NETWORK_ID, type = CommandType.UUID, entityType = NetworkResponse.class, required = true, + description = "the ID of the associated network to remove from the NIC") + private Long networkId; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public Long getNicId() { + return nicId; + } + + public Long getNetworkId() { + return networkId; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation /////////////////// + ///////////////////////////////////////////////////// + + @Override + public String getCommandName() { + return s_name; + } + + @Override + public String getEventType() { + return EventTypes.EVENT_NIC_NETWORK_DISASSOCIATE; + } + + @Override + public String getEventDescription() { + return "Disassociating network " + getResourceUuid(ApiConstants.NETWORK_ID) + " from 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 ResourceUnavailableException { + CallContext.current().setEventDetails("Nic ID: " + getResourceUuid(ApiConstants.NIC_ID)); + Nic result = _networkService.disassociateNetworkFromNic(this); + if (result == null) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to disassociate network from NIC. Refer to server logs for details."); + } + NicResponse response = _responseGenerator.createNicResponse(result); + response.setResponseName(getCommandName()); + setResponseObject(response); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/AssignVMCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/AssignVMCmd.java index 50ff97ac676f..ce2bd6c8eeb2 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/AssignVMCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/AssignVMCmd.java @@ -16,7 +16,9 @@ // under the License. package org.apache.cloudstack.api.command.admin.vm; +import java.util.ArrayList; import java.util.List; +import java.util.Map; import org.apache.cloudstack.api.ApiCommandResourceType; @@ -27,11 +29,15 @@ import org.apache.cloudstack.api.Parameter; import org.apache.cloudstack.api.ResponseObject.ResponseView; import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.command.user.vm.BaseDeployVMCmd; +import org.apache.cloudstack.api.command.user.vm.BaseDeployVMCmd.NicNetworkGrouping; import org.apache.cloudstack.api.response.DomainResponse; import org.apache.cloudstack.api.response.NetworkResponse; import org.apache.cloudstack.api.response.ProjectResponse; import org.apache.cloudstack.api.response.SecurityGroupResponse; import org.apache.cloudstack.api.response.UserVmResponse; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.collections.MapUtils; import com.cloud.exception.InvalidParameterValueException; import com.cloud.user.Account; @@ -77,6 +83,13 @@ public class AssignVMCmd extends BaseCmd { "In case there is no network yet created for the new account the default network will be created.") private List networkIds; + @Parameter(name = ApiConstants.NIC_NETWORKS_LIST, type = CommandType.MAP, since = "24.0.0", + description = "one entry per nic, each naming the set of networks that nic is associated with as a multi-VLAN trunk." + + " The first network id in each entry is the nic's primary network. Carries a trunk nic's associations across" + + " this move instead of rebuilding them as separate nics. Can't be specified with networkIds." + + " Example: nicnetworkslist[0].networkids=uuid1,uuid2,uuid3") + private Map nicNetworksList; + @Parameter(name = ApiConstants.SECURITY_GROUP_IDS, type = CommandType.LIST, collectionType = CommandType.UUID, @@ -109,9 +122,28 @@ public Long getProjectId() { } public List getNetworkIds() { + if (MapUtils.isNotEmpty(nicNetworksList)) { + if (CollectionUtils.isNotEmpty(networkIds)) { + throw new InvalidParameterValueException(String.format("%s can't be specified along with %s", + ApiConstants.NIC_NETWORKS_LIST, ApiConstants.NETWORK_IDS)); + } + List primaryNetworkIds = new ArrayList<>(); + for (NicNetworkGrouping grouping : getNicNetworksList()) { + primaryNetworkIds.add(grouping.getPrimaryNetworkId()); + } + return primaryNetworkIds; + } return networkIds; } + /** + * One entry per requested nic, each naming its full network association set (primary first). Empty when + * nicnetworkslist wasn't supplied, in which case the plain networkIds param keeps its exact present meaning. + */ + public List getNicNetworksList() { + return BaseDeployVMCmd.parseNicNetworksList(nicNetworksList, _networkService); + } + public List getSecurityGroupIdList() { return securityGroupIdList; } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/BaseDeployVMCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/BaseDeployVMCmd.java index 8d79ac4af45f..891912851938 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/BaseDeployVMCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/BaseDeployVMCmd.java @@ -19,7 +19,10 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; +import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; @@ -61,6 +64,7 @@ import com.cloud.hypervisor.Hypervisor.HypervisorType; import com.cloud.network.Network; import com.cloud.network.Network.IpAddresses; +import com.cloud.network.NetworkService; import com.cloud.offering.DiskOffering; import com.cloud.template.VirtualMachineTemplate; import com.cloud.utils.net.Dhcp; @@ -190,6 +194,15 @@ public abstract class BaseDeployVMCmd extends BaseAsyncCreateCustomIdCmd impleme + " Example: iptonetworklist[0].ip=10.10.10.11&iptonetworklist[0].ipv6=fc00:1234:5678::abcd&iptonetworklist[0].networkid=uuid&iptonetworklist[0].mac=aa:bb:cc:dd:ee::ff - requests to use ip 10.10.10.11 in network id=uuid") private Map ipToNetworkList; + @Parameter(name = ApiConstants.NIC_NETWORKS_LIST, type = CommandType.MAP, since = "24.0.0", + authorized = {RoleType.Admin}, + description = "one entry per nic, each naming the set of networks that nic is associated with as a multi-VLAN trunk. The first network id in each entry is the nic's primary network." + + " Can't be specified with networkIds or ipToNetworkList parameters." + + " Optional ip4addresses/ip6addresses request specific IPs for the entry's associated (non-primary) networks - comma-separated," + + " positionally aligned to networkids after its first (primary) entry; a blank token auto-allocates that network's IP, same as omitting it." + + " Example: nicnetworkslist[0].networkids=uuid1,uuid2,uuid3&nicnetworkslist[0].ip4address=10.10.10.11&nicnetworkslist[0].ip4addresses=10.10.20.5,10.10.30.5") + private Map nicNetworksList; + @Parameter(name = ApiConstants.IP_ADDRESS, type = CommandType.STRING, description = "the ip address for default vm's network") private String ipAddress; @@ -510,6 +523,18 @@ public VMLeaseManager.ExpiryAction getLeaseExpiryAction() { } public List getNetworkIds() { + if (MapUtils.isNotEmpty(nicNetworksList)) { + if (CollectionUtils.isNotEmpty(networkIds) || ipAddress != null || getIp6Address() != null + || MapUtils.isNotEmpty(ipToNetworkList) || MapUtils.isNotEmpty(vAppNetworks)) { + throw new InvalidParameterValueException(String.format("%s can't be specified along with %s, %s, %s, %s", + ApiConstants.NIC_NETWORKS_LIST, ApiConstants.NETWORK_IDS, ApiConstants.IP_ADDRESS, ApiConstants.IP_NETWORK_LIST, ApiConstants.NIC_NETWORK_LIST)); + } + List networks = new ArrayList<>(); + for (NicNetworkGrouping grouping : getNicNetworksList()) { + networks.add(grouping.getPrimaryNetworkId()); + } + return networks; + } if (MapUtils.isNotEmpty(vAppNetworks)) { if (CollectionUtils.isNotEmpty(networkIds) || ipAddress != null || getIp6Address() != null || MapUtils.isNotEmpty(ipToNetworkList)) { throw new InvalidParameterValueException(String.format("%s can't be specified along with %s, %s, %s", ApiConstants.NIC_NETWORK_LIST, ApiConstants.NETWORK_IDS, ApiConstants.IP_ADDRESS, ApiConstants.IP_NETWORK_LIST)); @@ -624,6 +649,20 @@ public boolean getStartVm() { } public Map getIpToNetworkMap() { + if (MapUtils.isNotEmpty(nicNetworksList)) { + LinkedHashMap ipToNetworkMap = new LinkedHashMap<>(); + for (NicNetworkGrouping grouping : getNicNetworksList()) { + if (grouping.getIp4Address() == null && grouping.getIp6Address() == null && grouping.getMacAddress() == null) { + continue; + } + HashMap ips = new HashMap<>(); + ips.put("ip", grouping.getIp4Address()); + ips.put("ipv6", grouping.getIp6Address()); + ips.put("mac", grouping.getMacAddress()); + ipToNetworkMap.put(grouping.getPrimaryNetworkId(), getIpAddressesFromIpMap(ips)); + } + return ipToNetworkMap.isEmpty() ? null : ipToNetworkMap; + } if ((networkIds != null || ipAddress != null || getIp6Address() != null) && ipToNetworkList != null) { throw new InvalidParameterValueException("NetworkIds and ipAddress can't be specified along with ipToNetworkMap parameter"); } @@ -643,6 +682,145 @@ public Map getIpToNetworkMap() { return ipToNetworkMap; } + /** + * Parses nicnetworkslist into one grouping per requested nic, each naming the full ordered set of networks + * (primary first) that nic is associated with. Legacy networkIds/ipToNetworkList/vAppNetworks are untouched + * when nicnetworkslist is absent - this only returns a non-empty list when it was actually supplied. + *

+ * Entries are read in ascending order of their nicnetworkslist[N] index, not map iteration order - the index + * is what both the deploy-time nic creation order (device id) and VNF's per-device-id validation rely on, and + * the underlying request map is not guaranteed to preserve insertion order. + */ + public List getNicNetworksList() { + return parseNicNetworksList(nicNetworksList, _networkService); + } + + /** + * Shared with {@link org.apache.cloudstack.api.command.admin.vm.AssignVMCmd}, which reuses this exact + * nicnetworkslist parameter shape so a trunk nic's associations can be carried across an account + * reassignment instead of being silently rebuilt as separate nics. + */ + public static List parseNicNetworksList(Map rawNicNetworksList, NetworkService networkService) { + List groupings = new ArrayList<>(); + if (MapUtils.isEmpty(rawNicNetworksList)) { + return groupings; + } + // the request-binding framework hands back these keys as Integer, not String - sort on the parsed value + // directly rather than assuming a type, so this works regardless of which one it actually is + List indices = new ArrayList<>(rawNicNetworksList.keySet()); + try { + indices.sort(Comparator.comparingInt(index -> Integer.parseInt(index.toString()))); + } catch (NumberFormatException e) { + throw new InvalidParameterValueException(String.format("%s indices must be integers", ApiConstants.NIC_NETWORKS_LIST)); + } + for (Object index : indices) { + HashMap entry = (HashMap) rawNicNetworksList.get(index); + String networkIdsCsv = entry.get("networkids"); + if (StringUtils.isBlank(networkIdsCsv)) { + throw new InvalidParameterValueException(String.format("%s entries must specify networkids", ApiConstants.NIC_NETWORKS_LIST)); + } + List resolvedNetworkIds = new ArrayList<>(); + for (String token : networkIdsCsv.split(",")) { + resolvedNetworkIds.add(resolveNetworkId(token.trim(), networkService)); + } + if (resolvedNetworkIds.size() != new HashSet<>(resolvedNetworkIds).size()) { + throw new InvalidParameterValueException(String.format("%s entry lists the same network more than once: %s", ApiConstants.NIC_NETWORKS_LIST, networkIdsCsv)); + } + Map associatedNetworkIps = parseAssociatedNetworkIps(resolvedNetworkIds, entry.get("ip4addresses"), entry.get("ip6addresses")); + groupings.add(new NicNetworkGrouping(resolvedNetworkIds, entry.get("ip4address"), entry.get("ip6address"), entry.get("macaddress"), associatedNetworkIps)); + } + return groupings; + } + + @Nonnull + private static Long resolveNetworkId(String networkIdOrUuid, NetworkService networkService) { + Network network = networkService.getNetwork(networkIdOrUuid); + if (network != null) { + return network.getId(); + } + try { + return Long.parseLong(networkIdOrUuid); + } catch (NumberFormatException e) { + throw new InvalidParameterValueException("Unable to translate and find entity with networkId: " + networkIdOrUuid); + } + } + + /** + * Parses a nicnetworkslist[N] entry's optional ip4addresses/ip6addresses - comma-separated, positionally + * aligned to that entry's networkids starting from its 2nd id (the associated networks; the primary's own + * ip4address/ip6address is handled separately). A blank token, or a shorter list than the associated-network + * count, leaves that network to auto-allocate - same as not requesting an IP for it at all. + */ + private static Map parseAssociatedNetworkIps(List networkIds, String ip4AddressesCsv, String ip6AddressesCsv) { + if (StringUtils.isBlank(ip4AddressesCsv) && StringUtils.isBlank(ip6AddressesCsv)) { + return Collections.emptyMap(); + } + List associatedNetworkIds = networkIds.subList(1, networkIds.size()); + String[] ip4Tokens = ip4AddressesCsv != null ? ip4AddressesCsv.split(",", -1) : new String[0]; + String[] ip6Tokens = ip6AddressesCsv != null ? ip6AddressesCsv.split(",", -1) : new String[0]; + if (ip4Tokens.length > associatedNetworkIds.size() || ip6Tokens.length > associatedNetworkIds.size()) { + throw new InvalidParameterValueException(String.format( + "%s entry's ip4addresses/ip6addresses can't list more entries than its associated networks (%d)", + ApiConstants.NIC_NETWORKS_LIST, associatedNetworkIds.size())); + } + Map associatedNetworkIps = new HashMap<>(); + for (int i = 0; i < associatedNetworkIds.size(); i++) { + String ip4 = i < ip4Tokens.length ? StringUtils.trimToNull(ip4Tokens[i]) : null; + String ip6 = i < ip6Tokens.length ? StringUtils.trimToNull(ip6Tokens[i]) : null; + if (ip4 != null || ip6 != null) { + associatedNetworkIps.put(associatedNetworkIds.get(i), new IpAddresses(ip4, ip6)); + } + } + return associatedNetworkIps; + } + + /** + * One requested nic's full network association set, primary network first, from a nicnetworkslist entry. + */ + public static class NicNetworkGrouping { + private final List networkIds; + private final String ip4Address; + private final String ip6Address; + private final String macAddress; + private final Map associatedNetworkIps; + + public NicNetworkGrouping(List networkIds, String ip4Address, String ip6Address, String macAddress, Map associatedNetworkIps) { + this.networkIds = networkIds; + this.ip4Address = ip4Address; + this.ip6Address = ip6Address; + this.macAddress = macAddress; + this.associatedNetworkIps = associatedNetworkIps; + } + + public Long getPrimaryNetworkId() { + return networkIds.get(0); + } + + public List getAssociatedNetworkIds() { + return networkIds.subList(1, networkIds.size()); + } + + public String getIp4Address() { + return ip4Address; + } + + public String getIp6Address() { + return ip6Address; + } + + public String getMacAddress() { + return macAddress; + } + + /** + * Requested ip4/ip6 addresses for this entry's associated (non-primary) networks, keyed by network id. + * A network with no entry here auto-allocates, same as today. + */ + public Map getAssociatedNetworkIps() { + return associatedNetworkIps; + } + } + @Nonnull private IpAddresses getIpAddressesFromIpMap(HashMap ips) { String requestedIp = ips.get("ip"); diff --git a/api/src/main/java/org/apache/cloudstack/api/response/HostForMigrationResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/HostForMigrationResponse.java index b3853d9cc245..6143ce8de725 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/HostForMigrationResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/HostForMigrationResponse.java @@ -29,7 +29,15 @@ public class HostForMigrationResponse extends HostResponse { @Param(description = "True if migrating an Instance to this host requires storage motion, false otherwise") private Boolean requiresStorageMotion; + @SerializedName("unsuitablereason") + @Param(description = "If this host is not suitable for migration, the reason why; null when suitable") + private String unsuitableReason; + public void setRequiresStorageMotion(Boolean requiresStorageMotion) { this.requiresStorageMotion = requiresStorageMotion; } + + public void setUnsuitableReason(String unsuitableReason) { + this.unsuitableReason = unsuitableReason; + } } 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 10bd62804fb2..52804cc980cd 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 void setInstanceConversionSupported(Boolean instanceConversionSupported) 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 Boolean getInstanceConversionSupported() { return instanceConversionSupported; } + public Boolean getVlanFilteringEnabled() { + return vlanFilteringEnabled; + } + + public Boolean getVlanTrunkXmlSupported() { + return vlanTrunkXmlSupported; + } + public void setExtensionId(String extensionId) { this.extensionId = extensionId; } diff --git a/api/src/main/java/org/apache/cloudstack/api/response/NicNetworkMapResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/NicNetworkMapResponse.java new file mode 100644 index 000000000000..45d5098c3814 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/response/NicNetworkMapResponse.java @@ -0,0 +1,137 @@ +// 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.response; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseResponse; +import org.apache.cloudstack.api.EntityReference; + +import com.cloud.serializer.Param; +import com.google.gson.annotations.SerializedName; + +@EntityReference(value = NicNetworkMapResponse.class) +@SuppressWarnings("unused") +public class NicNetworkMapResponse extends BaseResponse { + + @SerializedName(ApiConstants.NETWORK_ID) + @Param(description = "The ID of the associated network") + private String networkId; + + @SerializedName(ApiConstants.NETWORK_NAME) + @Param(description = "The name of the associated network") + private String networkName; + + @SerializedName(ApiConstants.BROADCAST_URI) + @Param(description = "The broadcast URI of the associated network") + private String broadcastUri; + + @SerializedName(ApiConstants.GATEWAY) + @Param(description = "The gateway of the associated network") + private String gateway; + + @SerializedName(ApiConstants.NETMASK) + @Param(description = "The netmask of the associated network") + private String netmask; + + @SerializedName(ApiConstants.IP6_GATEWAY) + @Param(description = "The IPv6 gateway of the associated network") + private String ip6Gateway; + + @SerializedName(ApiConstants.IP6_CIDR) + @Param(description = "The IPv6 CIDR of the associated network") + private String ip6Cidr; + + @SerializedName(ApiConstants.IP_ADDRESS) + @Param(description = "The IPv4 address on the associated network") + private String ipAddress; + + @SerializedName(ApiConstants.IP6_ADDRESS) + @Param(description = "The IPv6 address on the associated network") + private String ip6Address; + + public String getNetworkId() { + return networkId; + } + + public void setNetworkId(String networkId) { + this.networkId = networkId; + } + + public String getNetworkName() { + return networkName; + } + + public void setNetworkName(String networkName) { + this.networkName = networkName; + } + + public String getBroadcastUri() { + return broadcastUri; + } + + public void setBroadcastUri(String broadcastUri) { + this.broadcastUri = broadcastUri; + } + + public String getGateway() { + return gateway; + } + + public void setGateway(String gateway) { + this.gateway = gateway; + } + + public String getNetmask() { + return netmask; + } + + public void setNetmask(String netmask) { + this.netmask = netmask; + } + + public String getIp6Gateway() { + return ip6Gateway; + } + + public void setIp6Gateway(String ip6Gateway) { + this.ip6Gateway = ip6Gateway; + } + + public String getIp6Cidr() { + return ip6Cidr; + } + + public void setIp6Cidr(String ip6Cidr) { + this.ip6Cidr = ip6Cidr; + } + + public String getIpAddress() { + return ipAddress; + } + + public void setIpAddress(String ipAddress) { + this.ipAddress = ipAddress; + } + + public String getIp6Address() { + return ip6Address; + } + + public void setIp6Address(String ip6Address) { + this.ip6Address = ip6Address; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/response/NicResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/NicResponse.java index 5ad41ad62244..96d20b1b25b9 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/NicResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/NicResponse.java @@ -94,6 +94,14 @@ public class NicResponse extends BaseResponse { @Param(description = "The Secondary IPv4 addr of NIC") private List secondaryIps; + @SerializedName(ApiConstants.TRUNKED) + @Param(description = "True if this nic is a multi-VLAN trunk nic (associated with additional networks beyond its primary)", since = "24.0.0") + private Boolean trunked; + + @SerializedName(ApiConstants.ASSOCIATED_NETWORKS) + @Param(description = "The additional networks this nic is associated with as a multi-VLAN trunk", since = "24.0.0") + private List associatedNetworks; + @SerializedName(ApiConstants.EXTRA_DHCP_OPTION) @Param(description = "The extra DHCP options on the NIC", since = "4.11.0") private List extraDhcpOptions; @@ -269,6 +277,14 @@ public void setSecondaryIps(List ipList) { this.secondaryIps = ipList; } + public void setTrunked(Boolean trunked) { + this.trunked = trunked; + } + + public void setAssociatedNetworks(List associatedNetworks) { + this.associatedNetworks = associatedNetworks; + } + public void setNsxLogicalSwitch(String nsxLogicalSwitch) { this.nsxLogicalSwitch = nsxLogicalSwitch; } @@ -337,6 +353,14 @@ public List getSecondaryIps() { return secondaryIps; } + public Boolean getTrunked() { + return trunked; + } + + public List getAssociatedNetworks() { + return associatedNetworks; + } + public String getDeviceId() { return deviceId; } diff --git a/api/src/main/java/org/apache/cloudstack/storage/template/VnfTemplateManager.java b/api/src/main/java/org/apache/cloudstack/storage/template/VnfTemplateManager.java index 3df59811561b..de0cebd3a3a0 100644 --- a/api/src/main/java/org/apache/cloudstack/storage/template/VnfTemplateManager.java +++ b/api/src/main/java/org/apache/cloudstack/storage/template/VnfTemplateManager.java @@ -26,6 +26,7 @@ import com.cloud.uservm.UserVm; import org.apache.cloudstack.api.command.user.template.RegisterVnfTemplateCmd; import org.apache.cloudstack.api.command.user.template.UpdateVnfTemplateCmd; +import org.apache.cloudstack.api.command.user.vm.BaseDeployVMCmd; import org.apache.cloudstack.api.command.user.vm.DeployVnfApplianceCmd; import org.apache.cloudstack.framework.config.ConfigKey; import java.util.List; @@ -45,6 +46,20 @@ public interface VnfTemplateManager { void validateVnfApplianceNics(VirtualMachineTemplate template, List networkIds, Map vmNetworkMap); + /** + * Rejects a nicnetworkslist request that would make a VNF template's management nic a multi-VLAN trunk nic - + * management access must stay single-segment. No-op when nicNetworksList is empty (i.e. the legacy + * networkids/deploy-as-is paths, which cannot address a nic as a trunk at all). + */ + void validateVnfApplianceTrunkNics(VirtualMachineTemplate template, List nicNetworksList); + + /** + * Single-nic counterpart of {@link #validateVnfApplianceTrunkNics}, for the live associate-network-to-nic + * path (an already-deployed VNF's nic, rather than a deploy-time nicnetworkslist). No-op when the template + * has no VNF nic metadata at all, i.e. any non-VNF template. + */ + void validateVnfApplianceTrunkNic(VirtualMachineTemplate template, long deviceId); + SecurityGroup createSecurityGroupForVnfAppliance(DataCenter zone, VirtualMachineTemplate template, Account owner, DeployVnfApplianceCmd cmd); void createIsolatedNetworkRulesForVnfAppliance(DataCenter zone, VirtualMachineTemplate template, Account owner, diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/vm/DeployVMCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/vm/DeployVMCmdTest.java index 09d396e40237..d0b7e4fe94c9 100644 --- a/api/src/test/java/org/apache/cloudstack/api/command/user/vm/DeployVMCmdTest.java +++ b/api/src/test/java/org/apache/cloudstack/api/command/user/vm/DeployVMCmdTest.java @@ -265,6 +265,99 @@ public void testGetNetworkIdsIpToNetworkListAndNetworkIds() { assertTrue(thrownException.getMessage().contains("ipToNetworkMap can't be specified along with networkIds or ipAddress")); } + @Test + public void testGetNetworkIdsNicNetworksList() { + // the request-binding framework hands back these keys as Integer, not String + Map nicNetworksList = new HashMap<>(); + Map entry0 = new HashMap<>(); + entry0.put("networkids", "1,2,3"); + Map entry1 = new HashMap<>(); + entry1.put("networkids", "4"); + nicNetworksList.put(1, entry1); + nicNetworksList.put(0, entry0); + ReflectionTestUtils.setField(cmd, "nicNetworksList", nicNetworksList); + ReflectionTestUtils.setField(cmd, "networkIds", null); + ReflectionTestUtils.setField(cmd, "ipToNetworkList", null); + ReflectionTestUtils.setField(cmd, "vAppNetworks", null); + ReflectionTestUtils.setField(cmd, "ipAddress", null); + ReflectionTestUtils.setField(cmd, "ip6Address", null); + ReflectionTestUtils.setField(cmd, "_networkService", mock(NetworkService.class)); + + List result = cmd.getNetworkIds(); + + // primaries only, in ascending index order (0 then 1) despite being inserted out of order + assertEquals(Arrays.asList(1L, 4L), result); + } + + @Test + public void testGetNicNetworksListResolvesPrimaryAndAssociatedNetworkIds() { + Map nicNetworksList = new HashMap<>(); + Map entry0 = new HashMap<>(); + entry0.put("networkids", "1,2,3"); + nicNetworksList.put(0, entry0); + ReflectionTestUtils.setField(cmd, "nicNetworksList", nicNetworksList); + ReflectionTestUtils.setField(cmd, "_networkService", mock(NetworkService.class)); + + List result = cmd.getNicNetworksList(); + + assertEquals(1, result.size()); + assertEquals(Long.valueOf(1L), result.get(0).getPrimaryNetworkId()); + assertEquals(Arrays.asList(2L, 3L), result.get(0).getAssociatedNetworkIds()); + } + + @Test + public void testGetNicNetworksListParsesAssociatedNetworkIps() { + // ip4addresses/ip6addresses are positionally aligned to networkids after its first (primary) entry - + // here network 2 gets an explicit ip4/ip6, network 3 is left blank (auto-allocates) + Map nicNetworksList = new HashMap<>(); + Map entry0 = new HashMap<>(); + entry0.put("networkids", "1,2,3"); + entry0.put("ip4addresses", "10.1.1.5,"); + entry0.put("ip6addresses", "fd00::5,"); + nicNetworksList.put(0, entry0); + ReflectionTestUtils.setField(cmd, "nicNetworksList", nicNetworksList); + ReflectionTestUtils.setField(cmd, "_networkService", mock(NetworkService.class)); + + List result = cmd.getNicNetworksList(); + + Map associatedNetworkIps = result.get(0).getAssociatedNetworkIps(); + assertEquals(1, associatedNetworkIps.size()); + assertEquals("10.1.1.5", associatedNetworkIps.get(2L).getIp4Address()); + assertEquals("fd00::5", associatedNetworkIps.get(2L).getIp6Address()); + assertFalse(associatedNetworkIps.containsKey(3L)); + } + + @Test + public void testGetNicNetworksListRejectsTooManyAssociatedNetworkIps() { + Map nicNetworksList = new HashMap<>(); + Map entry0 = new HashMap<>(); + entry0.put("networkids", "1,2"); + entry0.put("ip4addresses", "10.1.1.5,10.1.1.6,10.1.1.7"); + nicNetworksList.put(0, entry0); + ReflectionTestUtils.setField(cmd, "nicNetworksList", nicNetworksList); + ReflectionTestUtils.setField(cmd, "_networkService", mock(NetworkService.class)); + + InvalidParameterValueException thrownException = assertThrows(InvalidParameterValueException.class, () -> { + cmd.getNicNetworksList(); + }); + assertTrue(thrownException.getMessage().contains("ip4addresses")); + } + + @Test + public void testGetNetworkIdsNicNetworksListAndNetworkIds() { + Map nicNetworksList = new HashMap<>(); + Map entry0 = new HashMap<>(); + entry0.put("networkids", "1,2"); + nicNetworksList.put(0, entry0); + ReflectionTestUtils.setField(cmd, "nicNetworksList", nicNetworksList); + ReflectionTestUtils.setField(cmd, "networkIds", Arrays.asList(1L, 2L)); + + InvalidParameterValueException thrownException = assertThrows(InvalidParameterValueException.class, () -> { + cmd.getNetworkIds(); + }); + assertTrue(thrownException.getMessage().contains("nicnetworkslist")); + } + @Test public void testGetIpToNetworkMap_WithNetworkIds() { ReflectionTestUtils.setField(cmd, "networkIds", Arrays.asList(1L, 2L)); diff --git a/core/src/main/java/com/cloud/agent/api/MigrateCommand.java b/core/src/main/java/com/cloud/agent/api/MigrateCommand.java index 7196247ffc23..eaaee660ba85 100644 --- a/core/src/main/java/com/cloud/agent/api/MigrateCommand.java +++ b/core/src/main/java/com/cloud/agent/api/MigrateCommand.java @@ -47,6 +47,11 @@ public class MigrateCommand extends Command { Map vlanToPersistenceMap = new HashMap<>(); + // null means unknown capability, not false + private Boolean destVlanFilteringEnabled; + private Boolean destVlanTrunkXmlSupported; + private Map nicBridgeMapping = new HashMap<>(); + public Map getDpdkInterfaceMapping() { return dpdkInterfaceMapping; } @@ -159,6 +164,30 @@ public void setClvmCrossPoolMigration(boolean clvmCrossPoolMigration) { this.clvmCrossPoolMigration = clvmCrossPoolMigration; } + public Boolean getDestVlanFilteringEnabled() { + return destVlanFilteringEnabled; + } + + public void setDestVlanFilteringEnabled(Boolean destVlanFilteringEnabled) { + this.destVlanFilteringEnabled = destVlanFilteringEnabled; + } + + public Boolean getDestVlanTrunkXmlSupported() { + return destVlanTrunkXmlSupported; + } + + public void setDestVlanTrunkXmlSupported(Boolean destVlanTrunkXmlSupported) { + this.destVlanTrunkXmlSupported = destVlanTrunkXmlSupported; + } + + public Map getNicBridgeMapping() { + return nicBridgeMapping; + } + + public void setNicBridgeMapping(Map nicBridgeMapping) { + this.nicBridgeMapping = nicBridgeMapping; + } + public static class MigrateDiskInfo { public enum DiskType { FILE, BLOCK; diff --git a/core/src/main/java/com/cloud/agent/api/PrepareForMigrationAnswer.java b/core/src/main/java/com/cloud/agent/api/PrepareForMigrationAnswer.java index 190e844ddc5f..ae6a824de966 100644 --- a/core/src/main/java/com/cloud/agent/api/PrepareForMigrationAnswer.java +++ b/core/src/main/java/com/cloud/agent/api/PrepareForMigrationAnswer.java @@ -30,6 +30,9 @@ public class PrepareForMigrationAnswer extends Answer { private Integer newVmCpuShares = null; + // destination bridge name per nic MAC, computed by the destination's own local capability + private Map nicBridgeMapping = new HashMap<>(); + protected PrepareForMigrationAnswer() { } @@ -60,4 +63,12 @@ public Integer getNewVmCpuShares() { public void setNewVmCpuShares(Integer newVmCpuShares) { this.newVmCpuShares = newVmCpuShares; } + + public Map getNicBridgeMapping() { + return nicBridgeMapping; + } + + public void setNicBridgeMapping(Map nicBridgeMapping) { + this.nicBridgeMapping = nicBridgeMapping; + } } diff --git a/core/src/main/java/com/cloud/agent/api/UpdateNicVlanMembershipAnswer.java b/core/src/main/java/com/cloud/agent/api/UpdateNicVlanMembershipAnswer.java new file mode 100644 index 000000000000..467298725a92 --- /dev/null +++ b/core/src/main/java/com/cloud/agent/api/UpdateNicVlanMembershipAnswer.java @@ -0,0 +1,30 @@ +// +// 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.agent.api; + +public class UpdateNicVlanMembershipAnswer extends Answer { + + protected UpdateNicVlanMembershipAnswer() { + } + + public UpdateNicVlanMembershipAnswer(UpdateNicVlanMembershipCommand command, boolean success, String details) { + super(command, success, details); + } +} diff --git a/core/src/main/java/com/cloud/agent/api/UpdateNicVlanMembershipCommand.java b/core/src/main/java/com/cloud/agent/api/UpdateNicVlanMembershipCommand.java new file mode 100644 index 000000000000..95c699802b75 --- /dev/null +++ b/core/src/main/java/com/cloud/agent/api/UpdateNicVlanMembershipCommand.java @@ -0,0 +1,54 @@ +// +// 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.agent.api; + +import com.cloud.agent.api.to.NicTO; + +/** + * Applies the full desired VLAN membership (primary + associated networks) to an already-plugged + * multi-VLAN trunk nic on a running Instance. Carries desired state, not a delta, so it is + * idempotent and safe to retry. + */ +public class UpdateNicVlanMembershipCommand extends Command { + + private NicTO nic; + private String instanceName; + + protected UpdateNicVlanMembershipCommand() { + } + + public UpdateNicVlanMembershipCommand(NicTO nic, String instanceName) { + this.nic = nic; + this.instanceName = instanceName; + } + + public NicTO getNic() { + return nic; + } + + public String getVmName() { + return instanceName; + } + + @Override + public boolean executeInSequence() { + return true; + } +} diff --git a/core/src/main/java/com/cloud/agent/api/VlanTrunkMigrationHelper.java b/core/src/main/java/com/cloud/agent/api/VlanTrunkMigrationHelper.java new file mode 100644 index 000000000000..ca1d2640fb56 --- /dev/null +++ b/core/src/main/java/com/cloud/agent/api/VlanTrunkMigrationHelper.java @@ -0,0 +1,87 @@ +// 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.agent.api; + +import java.util.Map; + +import org.apache.commons.collections.MapUtils; +import org.apache.logging.log4j.Logger; + +import com.cloud.agent.api.to.VirtualMachineTO; +import com.cloud.exception.AgentUnavailableException; +import com.cloud.exception.OperationTimedoutException; +import com.cloud.host.DetailVO; +import com.cloud.host.Host; +import com.cloud.host.dao.HostDetailsDao; +import com.cloud.network.Networks; +import com.cloud.vm.dao.NicDao; + +public class VlanTrunkMigrationHelper { + + // matches AgentManager.send's signature so callers can pass agentManager::send without core depending on AgentManager + @FunctionalInterface + public interface CommandSender { + Answer send(Long hostId, Command cmd) throws AgentUnavailableException, OperationTimedoutException; + } + + private VlanTrunkMigrationHelper() { + } + + public static void populateVlanTrunkMigrationDetails(MigrateCommand migrateCommand, PrepareForMigrationAnswer prepareForMigrationAnswer, + HostDetailsDao hostDetailsDao, long destHostId) { + Map nicBridgeMapping = prepareForMigrationAnswer.getNicBridgeMapping(); + if (MapUtils.isNotEmpty(nicBridgeMapping)) { + migrateCommand.setNicBridgeMapping(nicBridgeMapping); + } + + DetailVO destVlanFilteringDetail = hostDetailsDao.findDetail(destHostId, Host.HOST_VLAN_FILTERING_ENABLED); + migrateCommand.setDestVlanFilteringEnabled(destVlanFilteringDetail == null ? null : Boolean.parseBoolean(destVlanFilteringDetail.getValue())); + DetailVO destVlanTrunkXmlDetail = hostDetailsDao.findDetail(destHostId, Host.HOST_VLAN_TRUNK_XML_SUPPORTED); + migrateCommand.setDestVlanTrunkXmlSupported(destVlanTrunkXmlDetail == null ? null : Boolean.parseBoolean(destVlanTrunkXmlDetail.getValue())); + } + + public static boolean vmNeedsPostMigrationVlanTrunkMembership(long vmId, long dstHostId, NicDao nicDao, HostDetailsDao hostDetailsDao) { + DetailVO trunkXmlDetail = hostDetailsDao.findDetail(dstHostId, Host.HOST_VLAN_TRUNK_XML_SUPPORTED); + if (trunkXmlDetail != null && Boolean.parseBoolean(trunkXmlDetail.getValue())) { + return false; + } + DetailVO vlanFilteringDetail = hostDetailsDao.findDetail(dstHostId, Host.HOST_VLAN_FILTERING_ENABLED); + boolean destVlanFilteringEnabled = vlanFilteringDetail != null && Boolean.parseBoolean(vlanFilteringDetail.getValue()); + return nicDao.listByVmId(vmId).stream() + .anyMatch(nic -> nic.getBroadcastUri() != null && Networks.BroadcastDomainType.getSchemeValue(nic.getBroadcastUri()) == Networks.BroadcastDomainType.Vlan + && (nic.getMultiNetwork() || destVlanFilteringEnabled)); + } + + public static void sendPostMigrationVlanTrunkMembershipIfNeeded(VirtualMachineTO vmTO, long dstHostId, + NicDao nicDao, HostDetailsDao hostDetailsDao, CommandSender commandSender, Logger logger) { + if (!vmNeedsPostMigrationVlanTrunkMembership(vmTO.getId(), dstHostId, nicDao, hostDetailsDao)) { + return; + } + final String warningMsg = "Post-migration VLAN trunk membership tasks did not complete for VM [{}] " + + "on destination host [{}]: {}. Migration completed but VLAN membership may need manual correction."; + try { + PostMigrationCommand postMigrationCommand = new PostMigrationCommand(vmTO, vmTO.getName()); + Answer postMigrationAnswer = commandSender.send(dstHostId, postMigrationCommand); + if (postMigrationAnswer == null || !postMigrationAnswer.getResult()) { + String details = postMigrationAnswer != null ? postMigrationAnswer.getDetails() : "null answer returned"; + logger.warn(warningMsg, vmTO, dstHostId, details); + } + } catch (Exception e) { + logger.warn(warningMsg, vmTO, dstHostId, e.getMessage(), e); + } + } +} 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 67ca447e3853..cd7ab226fa72 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 void setDefault(boolean isDefault) { 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 041645046b23..d9faec0443e8 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 List generateConfig(final NetworkElementCommand cmd) { 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 1c43cd1823b9..d51c05b0a705 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 VmDhcpConfig() { 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 VmDhcpConfig(String hostName, String macAddress, String ipv4Address, Stri this.defaultEntry = defaultEntry; this.remove = remove; this.leaseTime = leaseTime; + this.associatedNetwork = associatedNetwork; } public String getHostName() { @@ -147,4 +151,12 @@ public void setLeaseTime(Long leaseTime) { this.leaseTime = leaseTime; } + public boolean isAssociatedNetwork() { + return associatedNetwork; + } + + public void setAssociatedNetwork(boolean associatedNetwork) { + this.associatedNetwork = associatedNetwork; + } + } diff --git a/engine/api/src/main/java/com/cloud/vm/VirtualMachineManager.java b/engine/api/src/main/java/com/cloud/vm/VirtualMachineManager.java index c7238a513693..77f3379ee708 100644 --- a/engine/api/src/main/java/com/cloud/vm/VirtualMachineManager.java +++ b/engine/api/src/main/java/com/cloud/vm/VirtualMachineManager.java @@ -88,6 +88,10 @@ public interface VirtualMachineManager extends Manager { ConfigKey AllowExposeDomainInMetadata = new ConfigKey<>("Advanced", Boolean.class, "metadata.allow.expose.domain", "false", "If set to true, it allows the VM's domain to be seen in metadata.", true, ConfigKey.Scope.Domain); + ConfigKey AllowExposeNicVlanMapping = new ConfigKey<>("Advanced", Boolean.class, "account.allow.expose.nic.vlan.mapping", + "false", "If set to true, exposes a VM's trunk NIC associated-network VLAN tag mappings via the metadata service, " + + "for guest/VNF boot scripts to configure VLAN sub-interfaces. Has no effect on VMs without trunk NICs.", true, ConfigKey.Scope.Account); + ConfigKey MetadataCustomCloudName = new ConfigKey<>("Advanced", String.class, "metadata.custom.cloud.name", "", "If provided, a custom cloud-name in cloud-init metadata", true, ConfigKey.Scope.Zone); diff --git a/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/NetworkOrchestrationService.java b/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/NetworkOrchestrationService.java index 109a44488ec4..5c141197ce12 100644 --- a/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/NetworkOrchestrationService.java +++ b/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/NetworkOrchestrationService.java @@ -98,6 +98,9 @@ public interface NetworkOrchestrationService { ConfigKey PromiscuousMode = new ConfigKey<>("Advanced", Boolean.class, "network.promiscuous.mode", "false", "Whether to allow or deny promiscuous mode on NICs for applicable network elements such as for vswitch/dvswitch portgroups.", true); + ConfigKey MultiNetworkNicEnabled = new ConfigKey<>("Advanced", Boolean.class, "multi.network.nic.enabled", "false", + "Allow a NIC to be associated with more than one network (multi-VLAN trunk NICs)", true, ConfigKey.Scope.Zone); + ConfigKey MacAddressChanges = new ConfigKey<>("Advanced", Boolean.class, "network.mac.address.changes", "true", "Whether to allow or deny mac address changes on NICs for applicable network elements such as for vswitch/dvswitch porgroups.", true); diff --git a/engine/components-api/src/main/java/com/cloud/deploy/DeploymentPlanningManager.java b/engine/components-api/src/main/java/com/cloud/deploy/DeploymentPlanningManager.java index ce40287bf033..25b8830fde8b 100644 --- a/engine/components-api/src/main/java/com/cloud/deploy/DeploymentPlanningManager.java +++ b/engine/components-api/src/main/java/com/cloud/deploy/DeploymentPlanningManager.java @@ -65,5 +65,7 @@ String finalizeReservation(DeployDestination plannedDestination, void checkForNonDedicatedResources(VirtualMachineProfile vmProfile, DataCenter dc, ExcludeList avoids); + void avoidHostsNotReadyForMultiNetworkNics(VirtualMachineProfile vmProfile, DataCenter dc, ExcludeList avoids); + void reorderHostsByPriority(Map priorities, List hosts); } diff --git a/engine/components-api/src/main/java/com/cloud/event/UsageEventUtils.java b/engine/components-api/src/main/java/com/cloud/event/UsageEventUtils.java index 1c88c7df124c..6209cc04b4a4 100644 --- a/engine/components-api/src/main/java/com/cloud/event/UsageEventUtils.java +++ b/engine/components-api/src/main/java/com/cloud/event/UsageEventUtils.java @@ -18,6 +18,7 @@ package com.cloud.event; import java.text.SimpleDateFormat; +import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.Map; @@ -26,6 +27,11 @@ import javax.inject.Inject; import com.cloud.network.Network; +import com.cloud.network.dao.NetworkDao; +import com.cloud.network.dao.NetworkVO; +import com.cloud.vm.Nic; +import com.cloud.vm.dao.NicNetworkMapDao; +import com.cloud.vm.dao.NicNetworkMapVO; import org.apache.commons.collections.MapUtils; import org.springframework.beans.factory.NoSuchBeanDefinitionException; @@ -48,6 +54,8 @@ public class UsageEventUtils { private static UsageEventDao s_usageEventDao; private static AccountDao s_accountDao; private static DataCenterDao s_dcDao; + private static NetworkDao s_networkDao; + private static NicNetworkMapDao s_nicNetworkMapDao; protected static Logger LOGGER = LogManager.getLogger(UsageEventUtils.class); protected static EventBus s_eventBus = null; protected static ConfigurationDao s_configDao; @@ -61,6 +69,10 @@ public class UsageEventUtils { DataCenterDao dcDao; @Inject ConfigurationDao configDao; + @Inject + NetworkDao networkDao; + @Inject + NicNetworkMapDao nicNetworkMapDao; public UsageEventUtils() { } @@ -71,6 +83,8 @@ void init() { s_accountDao = accountDao; s_dcDao = dcDao; s_configDao = configDao; + s_networkDao = networkDao; + s_nicNetworkMapDao = nicNetworkMapDao; } public static void publishUsageEvent(String usageType, long accountId, long zoneId, long resourceId, String resourceName, Long offeringId, Long templateId, @@ -94,6 +108,52 @@ public static void publishUsageEvent(String usageType, long accountId, long zone } + // named distinctly, rather than as another publishUsageEvent(...) overload, because its parameter shape + // (Map, boolean) is only distinguishable at compile time from the existing (Long, boolean) + // overload above when neither argument is a literal null - several existing call sites pass null literals + // here, which would make an overload ambiguous + public static void publishUsageEventWithDetails(String usageType, long accountId, long zoneId, long resourceId, String resourceName, Long offeringId, Long templateId, + Long size, String entityType, String entityUUID, Map details, boolean displayResource) { + if (displayResource) { + saveUsageEvent(usageType, accountId, zoneId, resourceId, resourceName, offeringId, templateId, size, details); + } + publishUsageEvent(usageType, accountId, zoneId, entityType, entityUUID); + } + + /** + * Emits one {@code usageType} usage event per network a NIC is actually billed against - its primary + * network, plus any additional networks it is associated with as a multi-VLAN trunk nic. An ordinary, + * non-trunk nic has exactly one such network, so this is identical to today's single-event behaviour for + * every legacy nic. Each event carries the network id as a detail, so the usage-processing side can + * disambiguate rows when a trunk nic's networks share the same network offering. + */ + public static void publishNicNetworkOfferingUsageEvents(String usageType, long accountId, long zoneId, long vmId, String entityType, String entityUUID, + Nic nic, long isDefault, boolean displayResource) { + NetworkVO primaryNetwork = s_networkDao.findById(nic.getNetworkId()); + if (primaryNetwork != null) { + publishNicNetworkOfferingUsageEvent(usageType, accountId, zoneId, vmId, entityType, entityUUID, nic.getId(), primaryNetwork, isDefault, displayResource); + } + if (nic.getMultiNetwork()) { + for (NicNetworkMapVO association : s_nicNetworkMapDao.listByNicId(nic.getId())) { + NetworkVO associatedNetwork = s_networkDao.findById(association.getNetworkId()); + if (associatedNetwork != null) { + publishNicNetworkOfferingUsageEvent(usageType, accountId, zoneId, vmId, entityType, entityUUID, nic.getId(), associatedNetwork, isDefault, + displayResource); + } + } + } + } + + // Public so a single association can be billed immediately outside the nic-wide loop above - e.g. a live + // associateNetworkToNic/disassociateNetworkFromNic on a Running VM, where only the one network just + // (dis)associated should be billed, not every network the nic already has. + public static void publishNicNetworkOfferingUsageEvent(String usageType, long accountId, long zoneId, long vmId, String entityType, String entityUUID, + long nicId, NetworkVO network, long isDefault, boolean displayResource) { + Map details = Collections.singletonMap(UsageEventVO.DynamicParameters.networkId.name(), String.valueOf(network.getId())); + publishUsageEventWithDetails(usageType, accountId, zoneId, vmId, Long.toString(nicId), network.getNetworkOfferingId(), null, isDefault, entityType, entityUUID, + details, displayResource); + } + public static void publishUsageEvent(String usageType, long accountId, long zoneId, long resourceId, String resourceName, Long offeringId, Long templateId, Long size, String entityType, String entityUUID, Long vmId, boolean displayResource) { if (displayResource) { diff --git a/engine/components-api/src/test/java/com/cloud/event/UsageEventUtilsTest.java b/engine/components-api/src/test/java/com/cloud/event/UsageEventUtilsTest.java new file mode 100644 index 000000000000..4b1c7b1bfd20 --- /dev/null +++ b/engine/components-api/src/test/java/com/cloud/event/UsageEventUtilsTest.java @@ -0,0 +1,212 @@ +// 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.event; + +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.inject.Inject; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +import com.cloud.dc.dao.DataCenterDao; +import com.cloud.event.dao.UsageEventDao; +import com.cloud.network.dao.NetworkDao; +import com.cloud.network.dao.NetworkVO; +import com.cloud.user.dao.AccountDao; +import com.cloud.vm.Nic; +import com.cloud.vm.dao.NicNetworkMapDao; +import com.cloud.vm.dao.NicNetworkMapVO; + +import org.apache.cloudstack.framework.config.dao.ConfigurationDao; + +/** + * Verifies {@link UsageEventUtils#publishNicNetworkOfferingUsageEvents} - the mechanism that emits one usage + * event per (nic, network) a nic actually bills against, rather than one per nic. The key non-regression + * assertion is that an ordinary, non-trunk nic still produces exactly the single event it always has. + */ +@RunWith(MockitoJUnitRunner.class) +public class UsageEventUtilsTest { + + private static final String USAGE_TYPE = EventTypes.EVENT_NETWORK_OFFERING_ASSIGN; + private static final long ACCOUNT_ID = 1L; + private static final long ZONE_ID = 2L; + private static final long VM_ID = 3L; + private static final long NIC_ID = 5L; + private static final String ENTITY_TYPE = "VirtualMachine"; + private static final String ENTITY_UUID = "vm-uuid"; + + protected Map staticFieldValues = new HashMap<>(); + + @Mock + protected UsageEventDao usageEventDao; + @Mock + protected AccountDao accountDao; + @Mock + protected DataCenterDao dcDao; + @Mock + protected ConfigurationDao configDao; + @Mock + protected NetworkDao networkDao; + @Mock + protected NicNetworkMapDao nicNetworkMapDao; + + @Before + public void setup() throws Exception { + staticFieldValues = new HashMap<>(); + UsageEventUtils utils = new UsageEventUtils(); + + for (Field field : UsageEventUtils.class.getDeclaredFields()) { + if (field.getAnnotation(Inject.class) != null) { + field.setAccessible(true); + try { + Field mockField = this.getClass().getDeclaredField(field.getName()); + mockField.setAccessible(true); + field.set(utils, mockField.get(this)); + Field staticField = UsageEventUtils.class.getDeclaredField("s_" + field.getName()); + staticField.setAccessible(true); + staticFieldValues.put(field.getName(), staticField.get(null)); + } catch (Exception e) { + // ignore fields with no matching mock/static counterpart + } + } + } + utils.init(); + + // configDao.getValue(...) defaults to null -> event-bus publish short-circuits without needing an + // EventDistributor bean; only the usage_event persistence path (what billing actually reads) matters here + Mockito.when(usageEventDao.persist(Mockito.any(UsageEventVO.class))).thenAnswer(invocation -> invocation.getArgument(0)); + } + + @After + public void teardown() throws Exception { + UsageEventUtils utils = new UsageEventUtils(); + for (String fieldName : staticFieldValues.keySet()) { + Field field = UsageEventUtils.class.getDeclaredField("s_" + fieldName); + field.setAccessible(true); + field.set(utils, staticFieldValues.get(fieldName)); + } + } + + private NetworkVO mockNetwork(long id, long offeringId) { + NetworkVO network = Mockito.mock(NetworkVO.class); + Mockito.when(network.getId()).thenReturn(id); + Mockito.when(network.getNetworkOfferingId()).thenReturn(offeringId); + return network; + } + + @Test + public void ordinaryNicEmitsExactlyOneEventForItsPrimaryNetworkOnly() { + Nic nic = Mockito.mock(Nic.class); + Mockito.when(nic.getId()).thenReturn(NIC_ID); + Mockito.when(nic.getNetworkId()).thenReturn(100L); + Mockito.when(nic.getMultiNetwork()).thenReturn(false); + NetworkVO primaryNetwork = mockNetwork(100L, 7L); + Mockito.when(networkDao.findById(100L)).thenReturn(primaryNetwork); + + UsageEventUtils.publishNicNetworkOfferingUsageEvents(USAGE_TYPE, ACCOUNT_ID, ZONE_ID, VM_ID, ENTITY_TYPE, ENTITY_UUID, nic, 1L, true); + + Mockito.verifyNoInteractions(nicNetworkMapDao); + + ArgumentCaptor eventCaptor = ArgumentCaptor.forClass(UsageEventVO.class); + Mockito.verify(usageEventDao, Mockito.times(1)).persist(eventCaptor.capture()); + UsageEventVO persisted = eventCaptor.getValue(); + Assert.assertEquals(Long.toString(NIC_ID), persisted.getResourceName()); + Assert.assertEquals(Long.valueOf(7L), persisted.getOfferingId()); + Assert.assertEquals(Long.valueOf(1L), persisted.getSize()); + + ArgumentCaptor> detailsCaptor = ArgumentCaptor.forClass(Map.class); + Mockito.verify(usageEventDao, Mockito.times(1)).saveDetails(Mockito.anyLong(), detailsCaptor.capture()); + Assert.assertEquals("100", detailsCaptor.getValue().get(UsageEventVO.DynamicParameters.networkId.name())); + } + + @Test + public void trunkNicEmitsOneEventForItsPrimaryNetworkAndOneForEachAssociatedNetwork() { + Nic nic = Mockito.mock(Nic.class); + Mockito.when(nic.getId()).thenReturn(NIC_ID); + Mockito.when(nic.getNetworkId()).thenReturn(100L); + Mockito.when(nic.getMultiNetwork()).thenReturn(true); + NetworkVO primaryNetwork = mockNetwork(100L, 7L); + NetworkVO associatedNetwork1 = mockNetwork(200L, 8L); + NetworkVO associatedNetwork2 = mockNetwork(300L, 9L); + Mockito.when(networkDao.findById(100L)).thenReturn(primaryNetwork); + Mockito.when(networkDao.findById(200L)).thenReturn(associatedNetwork1); + Mockito.when(networkDao.findById(300L)).thenReturn(associatedNetwork2); + List associations = Arrays.asList(new NicNetworkMapVO(NIC_ID, 200L), new NicNetworkMapVO(NIC_ID, 300L)); + Mockito.when(nicNetworkMapDao.listByNicId(NIC_ID)).thenReturn(associations); + + UsageEventUtils.publishNicNetworkOfferingUsageEvents(USAGE_TYPE, ACCOUNT_ID, ZONE_ID, VM_ID, ENTITY_TYPE, ENTITY_UUID, nic, 0L, true); + + ArgumentCaptor eventCaptor = ArgumentCaptor.forClass(UsageEventVO.class); + Mockito.verify(usageEventDao, Mockito.times(3)).persist(eventCaptor.capture()); + List offeringIds = eventCaptor.getAllValues().stream().map(UsageEventVO::getOfferingId).collect(java.util.stream.Collectors.toList()); + Assert.assertTrue(offeringIds.containsAll(Arrays.asList(7L, 8L, 9L))); + // every one of the three rows still belongs to the same nic + for (UsageEventVO event : eventCaptor.getAllValues()) { + Assert.assertEquals(Long.toString(NIC_ID), event.getResourceName()); + } + + ArgumentCaptor> detailsCaptor = ArgumentCaptor.forClass(Map.class); + Mockito.verify(usageEventDao, Mockito.times(3)).saveDetails(Mockito.anyLong(), detailsCaptor.capture()); + List networkIds = detailsCaptor.getAllValues().stream() + .map(details -> details.get(UsageEventVO.DynamicParameters.networkId.name())).collect(java.util.stream.Collectors.toList()); + Assert.assertTrue(networkIds.containsAll(Arrays.asList("100", "200", "300"))); + } + + @Test + public void displayResourceFalseNeverPersistsAnyUsageRow() { + Nic nic = Mockito.mock(Nic.class); + Mockito.when(nic.getId()).thenReturn(NIC_ID); + Mockito.when(nic.getNetworkId()).thenReturn(100L); + Mockito.when(nic.getMultiNetwork()).thenReturn(false); + NetworkVO primaryNetwork = mockNetwork(100L, 7L); + Mockito.when(networkDao.findById(100L)).thenReturn(primaryNetwork); + + UsageEventUtils.publishNicNetworkOfferingUsageEvents(USAGE_TYPE, ACCOUNT_ID, ZONE_ID, VM_ID, ENTITY_TYPE, ENTITY_UUID, nic, 1L, false); + + Mockito.verify(usageEventDao, Mockito.never()).persist(Mockito.any()); + Mockito.verify(usageEventDao, Mockito.never()).saveDetails(Mockito.anyLong(), Mockito.anyMap()); + } + + @Test + public void missingAssociatedNetworkIsSkippedRatherThanFailing() { + Nic nic = Mockito.mock(Nic.class); + Mockito.when(nic.getId()).thenReturn(NIC_ID); + Mockito.when(nic.getNetworkId()).thenReturn(100L); + Mockito.when(nic.getMultiNetwork()).thenReturn(true); + NetworkVO primaryNetwork = mockNetwork(100L, 7L); + Mockito.when(networkDao.findById(100L)).thenReturn(primaryNetwork); + Mockito.when(networkDao.findById(200L)).thenReturn(null); + Mockito.when(nicNetworkMapDao.listByNicId(NIC_ID)).thenReturn(Collections.singletonList(new NicNetworkMapVO(NIC_ID, 200L))); + + UsageEventUtils.publishNicNetworkOfferingUsageEvents(USAGE_TYPE, ACCOUNT_ID, ZONE_ID, VM_ID, ENTITY_TYPE, ENTITY_UUID, nic, 1L, true); + + Mockito.verify(usageEventDao, Mockito.times(1)).persist(Mockito.any()); + } +} 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 d3225f2fda5e..44ca78d08cea 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 @@ -817,8 +817,10 @@ protected AgentAttache notifyMonitorsOfConnection(final AgentAttache attache, fi 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, diskOnlyVmSnapshotNvramSupport, virtv2vVersion, ovftoolVersion, vddkSupport, vddkLibDir, vddkVersion)) { + if (ObjectUtils.anyNotNull(uefiEnabled, diskOnlyVmSnapshotNvramSupport, virtv2vVersion, ovftoolVersion, vddkSupport, vddkLibDir, vddkVersion, vlanFilteringEnabled, vlanTrunkXmlSupported)) { boolean updateNeeded = false; if (syncBooleanHostCapability(host, Host.HOST_UEFI_ENABLE, uefiEnabled)) { updateNeeded = true; @@ -838,6 +840,12 @@ protected AgentAttache notifyMonitorsOfConnection(final AgentAttache attache, fi host.getDetails().put(Host.HOST_VDDK_SUPPORT, vddkSupport); updateNeeded = true; } + if (syncBooleanHostCapability(host, Host.HOST_VLAN_FILTERING_ENABLED, vlanFilteringEnabled)) { + updateNeeded = true; + } + if (syncBooleanHostCapability(host, 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/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java b/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java index 8c9a1653a260..f432cf50228f 100755 --- a/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java +++ b/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java @@ -159,6 +159,7 @@ import com.cloud.agent.api.UnregisterVMCommand; import com.cloud.agent.api.UpdateVmNicAnswer; import com.cloud.agent.api.UpdateVmNicCommand; +import com.cloud.agent.api.VlanTrunkMigrationHelper; import com.cloud.agent.api.VmDiskStatsEntry; import com.cloud.agent.api.VmNetworkStatsEntry; import com.cloud.agent.api.VmStatsEntry; @@ -217,6 +218,7 @@ import com.cloud.exception.StorageUnavailableException; import com.cloud.ha.HighAvailabilityManager; import com.cloud.ha.HighAvailabilityManager.WorkType; +import com.cloud.host.DetailVO; import com.cloud.host.Host; import com.cloud.host.HostVO; import com.cloud.host.Status; @@ -1494,6 +1496,14 @@ public void orchestrateStart(final String vmUuid, final Map[] getConfigKeys() { VmOpLockStateRetry, VmOpWaitInterval, ExecuteInSequence, VmJobCheckInterval, VmJobTimeout, VmJobStateReportInterval, VmConfigDriveLabel, VmConfigDriveOnPrimaryPool, VmConfigDriveForceHostCacheUse, VmConfigDriveUseHostCacheOnUnsupportedPool, HaVmRestartHostUp, ResourceCountRunningVMsonly, AllowExposeHypervisorHostname, AllowExposeHypervisorHostnameAccountLevel, SystemVmRootDiskSize, - AllowExposeDomainInMetadata, MetadataCustomCloudName, VmMetadataManufacturer, VmMetadataProductName, + AllowExposeDomainInMetadata, AllowExposeNicVlanMapping, MetadataCustomCloudName, VmMetadataManufacturer, VmMetadataProductName, VmSyncPowerStateTransitioning, SystemVmEnableUserData }; } @@ -6551,6 +6574,10 @@ private boolean hasClvmVolumes(long vmId) { .anyMatch(pool -> pool != null && ClvmPoolManager.isClvmPoolType(pool.getPoolType())); } + boolean vmNeedsPostMigrationVlanTrunkMembership(VMInstanceVO vm, long dstHostId) { + return VlanTrunkMigrationHelper.vmNeedsPostMigrationVlanTrunkMembership(vm.getId(), dstHostId, _nicsDao, hostDetailsDao); + } + private void executePreMigrationCommand(VMInstanceVO vm, VirtualMachineTO to, long srcHostId) { if (!(vm.getHypervisorType() == HypervisorType.KVM && hasClvmVolumes(vm.getId()))) { return; 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 8af75562b31c..7901532b0b08 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 @@ -269,6 +269,8 @@ import com.cloud.vm.dao.NicExtraDhcpOptionDao; import com.cloud.vm.dao.NicIpAliasDao; import com.cloud.vm.dao.NicIpAliasVO; +import com.cloud.vm.dao.NicNetworkMapDao; +import com.cloud.vm.dao.NicNetworkMapVO; import com.cloud.vm.dao.NicSecondaryIpDao; import com.cloud.vm.dao.NicSecondaryIpVO; import com.cloud.vm.dao.UserVmDao; @@ -307,6 +309,8 @@ public class NetworkOrchestrator extends ManagerBase implements NetworkOrchestra @Inject NicDao _nicDao; @Inject + NicNetworkMapDao _nicNetworkMapDao; + @Inject RulesManager _rulesMgr; @Inject LoadBalancingRulesManager _lbMgr; @@ -2740,6 +2744,12 @@ && isDhcpAccrossMultipleSubnetsSupported(dhcpServiceProvider)) { if (!removeVmSecondaryIpsOfNic(nic.getId())) { logger.debug("Removing NIC {} secondary IP addresses failed", nic); } + + // remove any multi-VLAN trunk associations this nic held as a primary - otherwise they're left + // permanently orphaned, referencing a nic that no longer exists + for (final NicNetworkMapVO association : _nicNetworkMapDao.listByNicId(nic.getId())) { + _nicNetworkMapDao.remove(association.getId()); + } } public boolean isDhcpAccrossMultipleSubnetsSupported(final DhcpServiceProvider dhcpServiceProvider) { @@ -2791,6 +2801,9 @@ public void doInTransactionWithoutResult(final TransactionStatus status) { public void removeNics(final VirtualMachineProfile vm) { final List nics = _nicDao.listByVmId(vm.getId()); for (final NicVO nic : nics) { + for (final NicNetworkMapVO association : _nicNetworkMapDao.listByNicId(nic.getId())) { + _nicNetworkMapDao.remove(association.getId()); + } _nicDao.remove(nic.getId()); } } @@ -3477,6 +3490,13 @@ public boolean destroyNetwork(final long networkId, final ReservationContext con } } + // A network with no primary nics can still be actively used as a secondary (trunk) network by a nic + // whose primary is a different network entirely - nics.network_id alone would miss this + if (MultiNetworkNicEnabled.valueIn(network.getDataCenterId()) && !_nicNetworkMapDao.listByNetworkId(networkId).isEmpty()) { + logger.warn("Can't delete the Network {}, it is still associated with at least one NIC as a secondary network", network); + return false; + } + // Don't allow to delete network via api call when it has vms assigned to it final int nicCount = getActiveNicsInNetwork(networkId); if (nicCount > 0) { @@ -3715,6 +3735,15 @@ public void reallyRun() { continue; } + final NetworkVO network = _networksDao.findById(networkId); + if (network != null && MultiNetworkNicEnabled.valueIn(network.getDataCenterId()) + && !_nicNetworkMapDao.listByNetworkId(networkId).isEmpty()) { + // only zones with the feature enabled can ever have nic_network_map rows at all, + // so this check is skipped entirely everywhere else instead of querying needlessly + logger.debug("Network {} is still associated with at least one NIC as a secondary network, skipping", networkId); + continue; + } + final Long time = _lastNetworkIdsToFree.remove(networkId); if (time == null) { logger.debug("We found network {} to be free for the first time. Adding it to the list: {}", () -> _networksDao.findById(networkId), () -> currentTime); @@ -4101,6 +4130,9 @@ public List listVmNics(final long vmId, final Long nicId, final L result = _nicDao.listByVmId(vmId); } else { result = _nicDao.listByVmIdAndNicIdAndNtwkId(vmId, nicId, networkId); + if (networkId != null) { + result = addNicsWithNetworkAssociation(result, vmId, nicId, networkId); + } } } else { result = _nicDao.listByVmIdAndKeyword(vmId, keyword); @@ -4129,6 +4161,18 @@ public List listVmNics(final long vmId, final Long nicId, final L return result; } + // A nic whose primary network is something else but is also associated with networkId (multi-VLAN trunk nic) + // is invisible to the primary-only search above, so it needs a separate associations-based lookup here. + private List addNicsWithNetworkAssociation(final List primaryMatches, final long vmId, final Long nicId, final long networkId) { + final List result = new ArrayList<>(primaryMatches); + for (final NicVO candidate : _nicDao.listByVmIdAndNicIdAndNtwkId(vmId, nicId, null)) { + if (!result.contains(candidate) && _nicNetworkMapDao.findByNicIdAndNetworkId(candidate.getId(), networkId) != null) { + result.add(candidate); + } + } + return result; + } + @DB @Override public boolean reallocate(final VirtualMachineProfile vm, final DataCenterDeployment dest) throws InsufficientCapacityException, ConcurrentOperationException { @@ -5032,10 +5076,9 @@ public void unmanageNics(VirtualMachineProfile vm) { final List nics = _nicDao.listByVmId(vm.getId()); for (final NicVO nic : nics) { removeNic(vm, nic); - NetworkVO network = _networksDao.findById(nic.getNetworkId()); if (virtualMachine.getState() != VirtualMachine.State.Stopped) { - UsageEventUtils.publishUsageEvent(EventTypes.EVENT_NETWORK_OFFERING_REMOVE, virtualMachine.getAccountId(), virtualMachine.getDataCenterId(), virtualMachine.getId(), - Long.toString(nic.getId()), network.getNetworkOfferingId(), null, 0L, virtualMachine.getClass().getName(), virtualMachine.getUuid(), virtualMachine.isDisplay()); + UsageEventUtils.publishNicNetworkOfferingUsageEvents(EventTypes.EVENT_NETWORK_OFFERING_REMOVE, virtualMachine.getAccountId(), virtualMachine.getDataCenterId(), + virtualMachine.getId(), virtualMachine.getClass().getName(), virtualMachine.getUuid(), nic, 0L, virtualMachine.isDisplay()); } } } @@ -5069,6 +5112,6 @@ public ConfigKey[] getConfigKeys() { GuestDomainSuffix, NetworkThrottlingRate, VmNetworkThrottlingRate, MinVRVersion, DhcpLeaseTimeout, PromiscuousMode, MacAddressChanges, ForgedTransmits, MacLearning, RollingRestartEnabled, TUNGSTEN_ENABLED, NSX_ENABLED, NETRIS_ENABLED, NETWORK_LB_HAPROXY_MAX_CONN, - NETWORK_LB_HAPROXY_IDLE_TIMEOUT}; + NETWORK_LB_HAPROXY_IDLE_TIMEOUT, MultiNetworkNicEnabled}; } } diff --git a/engine/orchestration/src/test/java/com/cloud/vm/VirtualMachineManagerImplTest.java b/engine/orchestration/src/test/java/com/cloud/vm/VirtualMachineManagerImplTest.java index 3b680f0bc74a..fbd535c33d49 100644 --- a/engine/orchestration/src/test/java/com/cloud/vm/VirtualMachineManagerImplTest.java +++ b/engine/orchestration/src/test/java/com/cloud/vm/VirtualMachineManagerImplTest.java @@ -60,6 +60,7 @@ import com.cloud.ha.HighAvailabilityManager; import com.cloud.network.Network; import com.cloud.network.NetworkModel; +import com.cloud.network.Networks; import com.cloud.resource.ResourceManager; import com.cloud.storage.clvm.ClvmPoolManager; import org.apache.cloudstack.api.ApiConstants; @@ -126,9 +127,11 @@ import com.cloud.exception.AgentUnavailableException; import com.cloud.exception.InvalidParameterValueException; import com.cloud.exception.OperationTimedoutException; +import com.cloud.host.DetailVO; import com.cloud.host.Host; import com.cloud.host.HostVO; import com.cloud.host.dao.HostDao; +import com.cloud.host.dao.HostDetailsDao; import com.cloud.hypervisor.Hypervisor.HypervisorType; import com.cloud.hypervisor.HypervisorGuruManager; import com.cloud.network.NetworkService; @@ -284,6 +287,8 @@ public class VirtualMachineManagerImplTest { @Mock NicDao _nicsDao; @Mock + HostDetailsDao hostDetailsDao; + @Mock NetworkService networkService; @Mock NetworkModel networkModel; @@ -1298,6 +1303,158 @@ public void testOrchestrateStartNullPodId() throws Exception { assertNull(vmInstance.getPodIdToDeployIn()); } + @Test + public void testOrchestrateStartExcludesHostWithoutVlanFilteringForMultiNetworkNic() throws Exception { + VMInstanceVO vmInstance = new VMInstanceVO(); + ReflectionTestUtils.setField(vmInstance, "id", 1L); + ReflectionTestUtils.setField(vmInstance, "accountId", 1L); + ReflectionTestUtils.setField(vmInstance, "uuid", "vm-uuid"); + ReflectionTestUtils.setField(vmInstance, "serviceOfferingId", 2L); + ReflectionTestUtils.setField(vmInstance, "instanceName", "myVm"); + ReflectionTestUtils.setField(vmInstance, "hostId", 2L); + ReflectionTestUtils.setField(vmInstance, "type", VirtualMachine.Type.User); + ReflectionTestUtils.setField(vmInstance, "dataCenterId", 1L); + ReflectionTestUtils.setField(vmInstance, "hypervisorType", HypervisorType.KVM); + + VirtualMachineGuru vmGuru = mock(VirtualMachineGuru.class); + User user = mock(User.class); + Account account = mock(Account.class); + Account owner = mock(Account.class); + ReservationContext ctx = mock(ReservationContext.class); + ItWorkVO work = mock(ItWorkVO.class); + ServiceOfferingVO serviceOffering = mock(ServiceOfferingVO.class); + VirtualMachineTemplate template = mock(VirtualMachineTemplate.class); + when(template.isDeployAsIs()).thenReturn(false); + + DataCenterDeployment plan = mock(DataCenterDeployment.class); + when(plan.getDataCenterId()).thenReturn(1L); + when(plan.getPodId()).thenReturn(1L); + + Map params = new HashMap<>(); + DeploymentPlanner planner = mock(DeploymentPlanner.class); + + when(vmInstanceDaoMock.findByUuid("vm-uuid")).thenReturn(vmInstance); + doReturn(vmGuru).when(virtualMachineManagerImpl).getVmGuru(vmInstance); + + Ternary start = new Ternary<>(vmInstance, ctx, work); + Mockito.doReturn(start).when(virtualMachineManagerImpl).changeToStartState(vmGuru, vmInstance, user, account, owner, serviceOffering, template); + + when(ctx.getJournal()).thenReturn(Mockito.mock(Journal.class)); + when(serviceOfferingDaoMock.findById(vmInstance.getId(), vmInstance.getServiceOfferingId())).thenReturn(serviceOffering); + when(_entityMgr.findById(Account.class, vmInstance.getAccountId())).thenReturn(owner); + when(_entityMgr.findByIdIncludingRemoved(VirtualMachineTemplate.class, vmInstance.getTemplateId())).thenReturn(template); + + NicVO trunkNic = mock(NicVO.class); + when(trunkNic.getMultiNetwork()).thenReturn(true); + when(_nicsDao.listByVmId(vmInstance.getId())).thenReturn(java.util.Collections.singletonList(trunkNic)); + + Host notReadyHost = mock(Host.class); + when(notReadyHost.getId()).thenReturn(1L); + when(hostDetailsDao.findDetail(1L, Host.HOST_VLAN_FILTERING_ENABLED)).thenReturn(null); + + Host readyHost = mock(Host.class); + when(readyHost.getId()).thenReturn(2L); + DetailVO readyDetail = mock(DetailVO.class); + when(readyDetail.getValue()).thenReturn("true"); + when(hostDetailsDao.findDetail(2L, Host.HOST_VLAN_FILTERING_ENABLED)).thenReturn(readyDetail); + + Pod destPod = mock(Pod.class); + when(destPod.getId()).thenReturn(2L); + Cluster cluster = mock(Cluster.class); + when(cluster.getId()).thenReturn(1L); + + DeployDestination notReadyDest = mock(DeployDestination.class); + when(notReadyDest.getHost()).thenReturn(notReadyHost); + + DeployDestination readyDest = mock(DeployDestination.class); + when(readyDest.getHost()).thenReturn(readyHost); + when(readyDest.getPod()).thenReturn(destPod); + when(readyDest.getCluster()).thenReturn(cluster); + + when(_dpMgr.planDeployment(any(VirtualMachineProfileImpl.class), any(DataCenterDeployment.class), any(ExcludeList.class), any(DeploymentPlanner.class))) + .thenReturn(notReadyDest, readyDest); + + doNothing().when(virtualMachineManagerImpl).checkIfTemplateNeededForCreatingVmVolumes(vmInstance); + + when(_workDao.updateStep(any(), any())).thenReturn(true); + when(_stateMachine.transitTo(vmInstance, VirtualMachine.Event.OperationRetry, new Pair(vmInstance.getHostId(), 2L), vmInstanceDaoMock)).thenThrow(new CloudRuntimeException("Error while transitioning")); + when(_stateMachine.transitTo(vmInstance, VirtualMachine.Event.OperationFailed, new Pair(vmInstance.getHostId(), null), vmInstanceDaoMock)).thenReturn(true); + + ClusterDetailsVO cluster_detail_cpu = mock(ClusterDetailsVO.class); + ClusterDetailsVO cluster_detail_ram = mock(ClusterDetailsVO.class); + when(_clusterDetailsDao.findDetail(1L, VmDetailConstants.CPU_OVER_COMMIT_RATIO)).thenReturn(cluster_detail_cpu); + when(_clusterDetailsDao.findDetail(1L, VmDetailConstants.MEMORY_OVER_COMMIT_RATIO)).thenReturn(cluster_detail_ram); + when(vmInstanceDetailsDao.findDetail(anyLong(), Mockito.anyString())).thenReturn(null); + when(cluster_detail_cpu.getValue()).thenReturn("1.0"); + when(cluster_detail_ram.getValue()).thenReturn("1.0"); + doReturn(false).when(virtualMachineManagerImpl).areAllVolumesAllocated(Mockito.anyLong()); + + CallContext callContext = mock(CallContext.class); + when(callContext.getCallingAccount()).thenReturn(account); + when(callContext.getCallingUser()).thenReturn(user); + try (MockedStatic ignored = Mockito.mockStatic(CallContext.class)) { + when(CallContext.current()).thenReturn(callContext); + + try { + virtualMachineManagerImpl.orchestrateStart("vm-uuid", params, plan, planner); + } catch (CloudRuntimeException e) { + assertEquals(e.getMessage(), "Error while transitioning"); + } + } + + // first attempt landed on the not-ready host and was excluded without ever touching state transition; + // second attempt landed on the ready host and proceeded (reaching the same known stopping point as the + // other orchestrateStart tests confirms execution got past the readiness check this time) + verify(_dpMgr, Mockito.times(2)).planDeployment(any(VirtualMachineProfileImpl.class), any(DataCenterDeployment.class), any(ExcludeList.class), any(DeploymentPlanner.class)); + verify(_stateMachine, Mockito.never()).transitTo(vmInstance, VirtualMachine.Event.OperationRetry, new Pair(vmInstance.getHostId(), 1L), vmInstanceDaoMock); + verify(hostDetailsDao, Mockito.atLeastOnce()).findDetail(2L, Host.HOST_VLAN_FILTERING_ENABLED); + verify(_stateMachine, Mockito.atLeastOnce()).transitTo(vmInstance, VirtualMachine.Event.OperationRetry, new Pair(vmInstance.getHostId(), 2L), vmInstanceDaoMock); + assertEquals(vmInstance.getPodIdToDeployIn(), (Long) destPod.getId()); + } + + private VMInstanceVO mockVmWithId(long id) { + VMInstanceVO vm = mock(VMInstanceVO.class); + when(vm.getId()).thenReturn(id); + return vm; + } + + private NicVO mockVlanNic(int vlan, boolean multiNetwork) { + NicVO nic = mock(NicVO.class); + when(nic.getBroadcastUri()).thenReturn(Networks.BroadcastDomainType.Vlan.toUri(vlan)); + when(nic.getMultiNetwork()).thenReturn(multiNetwork); + return nic; + } + + @Test + public void vmNeedsPostMigrationVlanTrunkMembershipFalseWhenDestSupportsTrunkXml() { + DetailVO trunkXmlSupported = mock(DetailVO.class); + when(trunkXmlSupported.getValue()).thenReturn("true"); + when(hostDetailsDao.findDetail(2L, Host.HOST_VLAN_TRUNK_XML_SUPPORTED)).thenReturn(trunkXmlSupported); + + Assert.assertFalse(virtualMachineManagerImpl.vmNeedsPostMigrationVlanTrunkMembership(mockVmWithId(1L), 2L)); + verify(_nicsDao, Mockito.never()).listByVmId(anyLong()); + } + + @Test + public void vmNeedsPostMigrationVlanTrunkMembershipTrueForTrunkNicOnNonTrunkXmlDest() { + when(hostDetailsDao.findDetail(2L, Host.HOST_VLAN_TRUNK_XML_SUPPORTED)).thenReturn(null); + when(hostDetailsDao.findDetail(2L, Host.HOST_VLAN_FILTERING_ENABLED)).thenReturn(null); + NicVO trunkNic = mockVlanNic(100, true); + when(_nicsDao.listByVmId(1L)).thenReturn(java.util.Collections.singletonList(trunkNic)); + + Assert.assertTrue(virtualMachineManagerImpl.vmNeedsPostMigrationVlanTrunkMembership(mockVmWithId(1L), 2L)); + } + + @Test + public void vmNeedsPostMigrationVlanTrunkMembershipFalseForOrdinaryNicWithoutVlanFilteringDest() { + when(hostDetailsDao.findDetail(2L, Host.HOST_VLAN_TRUNK_XML_SUPPORTED)).thenReturn(null); + when(hostDetailsDao.findDetail(2L, Host.HOST_VLAN_FILTERING_ENABLED)).thenReturn(null); + NicVO ordinaryNic = mockVlanNic(100, false); + when(_nicsDao.listByVmId(1L)).thenReturn(java.util.Collections.singletonList(ordinaryNic)); + + Assert.assertFalse(virtualMachineManagerImpl.vmNeedsPostMigrationVlanTrunkMembership(mockVmWithId(1L), 2L)); + } + @Test public void testIsDiskOfferingSuitableForVmSuccess() { Mockito.doReturn(Mockito.mock(DiskOfferingVO.class)).when(diskOfferingDaoMock).findById(anyLong()); diff --git a/engine/orchestration/src/test/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestratorTest.java b/engine/orchestration/src/test/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestratorTest.java index 66f5b699cc46..5f4aa54019de 100644 --- a/engine/orchestration/src/test/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestratorTest.java +++ b/engine/orchestration/src/test/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestratorTest.java @@ -24,6 +24,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import java.lang.reflect.Field; import java.net.URI; import java.util.ArrayList; import java.util.Arrays; @@ -89,6 +90,7 @@ import com.cloud.vm.NicProfile; import com.cloud.vm.NicVO; import com.cloud.vm.ReservationContext; +import com.cloud.vm.UserVmVO; import com.cloud.vm.VirtualMachine; import com.cloud.vm.VirtualMachine.Type; import com.cloud.vm.VirtualMachineProfile; @@ -96,7 +98,12 @@ import com.cloud.vm.dao.NicDao; import com.cloud.vm.dao.NicExtraDhcpOptionDao; import com.cloud.vm.dao.NicIpAliasDao; +import com.cloud.vm.dao.NicNetworkMapDao; +import com.cloud.vm.dao.NicNetworkMapVO; import com.cloud.vm.dao.NicSecondaryIpDao; +import com.cloud.vm.dao.UserVmDao; +import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService; +import org.apache.cloudstack.framework.config.ConfigKey; import junit.framework.TestCase; @@ -143,6 +150,8 @@ public void setUp() { testOrchestrator._ipAddrMgr = mock(IpAddressManager.class); testOrchestrator._entityMgr = mock(EntityManager.class); testOrchestrator.extensionHelper = mock(ExtensionHelper.class); + testOrchestrator._userVmDao = mock(UserVmDao.class); + testOrchestrator._nicNetworkMapDao = mock(NicNetworkMapDao.class); networkExtensionElement = mock(NetworkExtensionElement.class); ReflectionTestUtils.setField(testOrchestrator, "networkExtensionElement", networkExtensionElement); DhcpServiceProvider provider = mock(DhcpServiceProvider.class); @@ -210,6 +219,42 @@ public void testDontRemoveDhcpServiceFromDomainRouter() { verify(testOrchestrator._ntwkSrvcDao, never()).getProviderForServiceInNetwork(network.getId(), Service.Dhcp); verify(testOrchestrator._networksDao, times(1)).findById(nic.getNetworkId()); } + + @Test + public void removeNicCleansUpOrphanedNicNetworkMapAssociations() { + VirtualMachineProfile vm = mock(VirtualMachineProfile.class); + NicVO nic = mock(NicVO.class); + NetworkVO network = mock(NetworkVO.class); + + when(vm.getType()).thenReturn(Type.DomainRouter); + when(nic.getId()).thenReturn(25L); + when(network.getGuruName()).thenReturn(guruName); + when(testOrchestrator._networksDao.findById(nic.getNetworkId())).thenReturn(network); + NicNetworkMapVO association = new NicNetworkMapVO(25L, 210L, "10.1.1.50", null); + when(testOrchestrator._nicNetworkMapDao.listByNicId(25L)).thenReturn(Collections.singletonList(association)); + + testOrchestrator.removeNic(vm, nic); + + verify(testOrchestrator._nicNetworkMapDao).remove(association.getId()); + } + + @Test + public void removeNicsCleansUpOrphanedNicNetworkMapAssociations() { + VirtualMachineProfile vm = mock(VirtualMachineProfile.class); + NicVO nic = mock(NicVO.class); + + when(vm.getId()).thenReturn(13L); + when(nic.getId()).thenReturn(25L); + when(testOrchestrator._nicDao.listByVmId(13L)).thenReturn(Collections.singletonList(nic)); + NicNetworkMapVO association = new NicNetworkMapVO(25L, 210L, "10.1.1.50", null); + when(testOrchestrator._nicNetworkMapDao.listByNicId(25L)).thenReturn(Collections.singletonList(association)); + + testOrchestrator.removeNics(vm); + + verify(testOrchestrator._nicNetworkMapDao).remove(association.getId()); + verify(testOrchestrator._nicDao).remove(25L); + } + @Test public void testDontRemoveDhcpServiceWhenNotProvided() { // make local mocks @@ -1073,4 +1118,54 @@ public void getNetworkElementsIncludingExtensionsReturnsBaseListWhenExtensionHel assertNotNull(result); assertEquals(1, result.size()); } + + private void overrideDefaultConfigValue(final ConfigKey configKey, final String name, final Object o) throws IllegalAccessException, NoSuchFieldException { + Field f = ConfigKey.class.getDeclaredField(name); + f.setAccessible(true); + f.set(configKey, o); + } + + @Test + public void destroyNetworkFailsWhenActivelyAssociatedAsSecondaryNetwork() throws Exception { + overrideDefaultConfigValue(NetworkOrchestrationService.MultiNetworkNicEnabled, "_defaultValue", "true"); + try { + NetworkVO network = mock(NetworkVO.class); + when(network.getId()).thenReturn(1L); + when(network.getDataCenterId()).thenReturn(1L); + when(testOrchestrator._networksDao.findById(1L)).thenReturn(network); + when(testOrchestrator._userVmDao.listByNetworkIdAndStates(1L)).thenReturn(new ArrayList()); + when(testOrchestrator._nicNetworkMapDao.listByNetworkId(1L)).thenReturn(Collections.singletonList(mock(NicNetworkMapVO.class))); + + boolean result = testOrchestrator.destroyNetwork(1L, mock(ReservationContext.class), false); + + assertFalse(result); + verify(testOrchestrator._networksDao, never()).getActiveNicsIn(ArgumentMatchers.anyLong()); + } finally { + overrideDefaultConfigValue(NetworkOrchestrationService.MultiNetworkNicEnabled, "_defaultValue", "false"); + } + } + + @Test + public void destroyNetworkSkipsAssociationCheckWhenFeatureDisabledInZone() throws Exception { + overrideDefaultConfigValue(NetworkOrchestrationService.MultiNetworkNicEnabled, "_defaultValue", "false"); + + NetworkVO network = mock(NetworkVO.class); + when(network.getId()).thenReturn(1L); + when(network.getDataCenterId()).thenReturn(1L); + when(testOrchestrator._networksDao.findById(1L)).thenReturn(network); + when(testOrchestrator._userVmDao.listByNetworkIdAndStates(1L)).thenReturn(new ArrayList()); + DataCenter zone = mock(DataCenter.class); + when(zone.getNetworkType()).thenReturn(DataCenter.NetworkType.Advanced); + when(testOrchestrator._entityMgr.findById(DataCenter.class, 1L)).thenReturn(zone); + + try { + testOrchestrator.destroyNetwork(1L, mock(ReservationContext.class), false); + } catch (Exception ignored) { + // destroyNetwork continues well beyond what this test cares about (network element shutdown, etc., + // none of it mocked here) - only the association-check skip behavior matters for this test + } + + verify(testOrchestrator._nicNetworkMapDao, never()).listByNetworkId(ArgumentMatchers.anyLong()); + verify(testOrchestrator._networksDao).getActiveNicsIn(1L); + } } diff --git a/engine/schema/src/main/java/com/cloud/event/UsageEventVO.java b/engine/schema/src/main/java/com/cloud/event/UsageEventVO.java index 41ecec0c7fb8..f80f00fdfa90 100644 --- a/engine/schema/src/main/java/com/cloud/event/UsageEventVO.java +++ b/engine/schema/src/main/java/com/cloud/event/UsageEventVO.java @@ -31,7 +31,7 @@ @Table(name = "usage_event") public class UsageEventVO implements UsageEvent { public enum DynamicParameters { - cpuSpeed, cpuNumber, memory, vmSnapshotId + cpuSpeed, cpuNumber, memory, vmSnapshotId, networkId }; @Id diff --git a/engine/schema/src/main/java/com/cloud/host/dao/HostDetailsDao.java b/engine/schema/src/main/java/com/cloud/host/dao/HostDetailsDao.java index b72ec5239ffb..e60a904d7e02 100644 --- a/engine/schema/src/main/java/com/cloud/host/dao/HostDetailsDao.java +++ b/engine/schema/src/main/java/com/cloud/host/dao/HostDetailsDao.java @@ -18,6 +18,7 @@ import java.util.List; import java.util.Map; +import java.util.Set; import com.cloud.host.DetailVO; import com.cloud.utils.db.GenericDao; @@ -37,4 +38,7 @@ public interface HostDetailsDao extends GenericDao { void replaceExternalDetails(long hostId, Map details); + // Ids of hosts with Host.HOST_VLAN_FILTERING_ENABLED=true, i.e. hosts able to run a multi-VLAN trunk nic. + Set findHostIdsWithVlanFilteringEnabled(); + } diff --git a/engine/schema/src/main/java/com/cloud/host/dao/HostDetailsDaoImpl.java b/engine/schema/src/main/java/com/cloud/host/dao/HostDetailsDaoImpl.java index 0f8e8623506d..cf0b801ddd95 100644 --- a/engine/schema/src/main/java/com/cloud/host/dao/HostDetailsDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/host/dao/HostDetailsDaoImpl.java @@ -22,10 +22,13 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; import org.springframework.stereotype.Component; import com.cloud.host.DetailVO; +import com.cloud.host.Host; import com.cloud.utils.crypt.DBEncryptionUtil; import com.cloud.utils.db.GenericDaoBase; import com.cloud.utils.db.SearchBuilder; @@ -139,6 +142,14 @@ public List findByName(String name) { return listBy(sc); } + @Override + public Set findHostIdsWithVlanFilteringEnabled() { + return findByName(Host.HOST_VLAN_FILTERING_ENABLED).stream() + .filter(detail -> Boolean.parseBoolean(detail.getValue())) + .map(DetailVO::getHostId) + .collect(Collectors.toSet()); + } + @Override public void removeExternalDetails(long hostId) { TransactionLegacy txn = TransactionLegacy.currentTxn(); diff --git a/engine/schema/src/main/java/com/cloud/usage/UsageNetworkOfferingVO.java b/engine/schema/src/main/java/com/cloud/usage/UsageNetworkOfferingVO.java index f16f1ea5ac1c..473f169d4d65 100644 --- a/engine/schema/src/main/java/com/cloud/usage/UsageNetworkOfferingVO.java +++ b/engine/schema/src/main/java/com/cloud/usage/UsageNetworkOfferingVO.java @@ -67,11 +67,19 @@ public class UsageNetworkOfferingVO implements InternalIdentity { @Column(name = "nic_id") private Long nicId; + @Column(name = "network_id") + private Long networkId; + protected UsageNetworkOfferingVO() { } public UsageNetworkOfferingVO(long zoneId, long accountId, long domainId, long vmInstanceId, long networkOfferingId, long nicId, boolean isDefault, Date created, Date deleted) { + this(zoneId, accountId, domainId, vmInstanceId, networkOfferingId, nicId, null, isDefault, created, deleted); + } + + public UsageNetworkOfferingVO(long zoneId, long accountId, long domainId, long vmInstanceId, long networkOfferingId, long nicId, Long networkId, boolean isDefault, + Date created, Date deleted) { this.zoneId = zoneId; this.accountId = accountId; this.domainId = domainId; @@ -81,6 +89,7 @@ public UsageNetworkOfferingVO(long zoneId, long accountId, long domainId, long v this.created = created; this.deleted = deleted; this.nicId = nicId; + this.networkId = networkId; } public long getZoneId() { @@ -127,6 +136,14 @@ public void setNicId(Long nicId) { this.nicId = nicId; } + public Long getNetworkId() { + return networkId; + } + + public void setNetworkId(Long networkId) { + this.networkId = networkId; + } + @Override public long getId() { return this.id; diff --git a/engine/schema/src/main/java/com/cloud/usage/dao/UsageNetworkOfferingDao.java b/engine/schema/src/main/java/com/cloud/usage/dao/UsageNetworkOfferingDao.java index 9f987b8897ac..a930abe9cb05 100644 --- a/engine/schema/src/main/java/com/cloud/usage/dao/UsageNetworkOfferingDao.java +++ b/engine/schema/src/main/java/com/cloud/usage/dao/UsageNetworkOfferingDao.java @@ -26,4 +26,10 @@ public interface UsageNetworkOfferingDao extends GenericDao getUsageRecords(Long accountId, Long domainId, Date startDate, Date endDate, boolean limit, int page); + + /** + * Backfills network_id on a nic's own currently-open, not-yet-populated usage row(s), the moment that nic + * is first converted to a multi-VLAN trunk nic. Historic rows are otherwise never backfilled in bulk. + */ + public void backfillNetworkIdForNic(long nicId, long networkId); } diff --git a/engine/schema/src/main/java/com/cloud/usage/dao/UsageNetworkOfferingDaoImpl.java b/engine/schema/src/main/java/com/cloud/usage/dao/UsageNetworkOfferingDaoImpl.java index b3bc06e8af40..e9095c498b23 100644 --- a/engine/schema/src/main/java/com/cloud/usage/dao/UsageNetworkOfferingDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/usage/dao/UsageNetworkOfferingDaoImpl.java @@ -37,16 +37,20 @@ public class UsageNetworkOfferingDaoImpl extends GenericDaoBase implements UsageNetworkOfferingDao { protected static final String UPDATE_DELETED = - "UPDATE usage_network_offering SET deleted = ? WHERE account_id = ? AND vm_instance_id = ? AND network_offering_id = ? and deleted IS NULL"; + "UPDATE usage_network_offering SET deleted = ? WHERE id = ? and deleted IS NULL"; protected static final String GET_USAGE_RECORDS_BY_ACCOUNT = - "SELECT zone_id, account_id, domain_id, vm_instance_id, network_offering_id, nic_id, is_default, created, deleted " + "FROM usage_network_offering " + "SELECT zone_id, account_id, domain_id, vm_instance_id, network_offering_id, nic_id, is_default, created, deleted, network_id " + "FROM usage_network_offering " + "WHERE account_id = ? AND ((deleted IS NULL) OR (created BETWEEN ? AND ?) OR " + " (deleted BETWEEN ? AND ?) OR ((created <= ?) AND (deleted >= ?)))"; protected static final String GET_USAGE_RECORDS_BY_DOMAIN = - "SELECT zone_id, account_id, domain_id, vm_instance_id, network_offering_id, nic_id, is_default, created, deleted " + "FROM usage_network_offering " + "SELECT zone_id, account_id, domain_id, vm_instance_id, network_offering_id, nic_id, is_default, created, deleted, network_id " + "FROM usage_network_offering " + "WHERE domain_id = ? AND ((deleted IS NULL) OR (created BETWEEN ? AND ?) OR " + " (deleted BETWEEN ? AND ?) OR ((created <= ?) AND (deleted >= ?)))"; protected static final String GET_ALL_USAGE_RECORDS = - "SELECT zone_id, account_id, domain_id, vm_instance_id, network_offering_id, nic_id, is_default, created, deleted " + "FROM usage_network_offering " + "SELECT zone_id, account_id, domain_id, vm_instance_id, network_offering_id, nic_id, is_default, created, deleted, network_id " + "FROM usage_network_offering " + "WHERE (deleted IS NULL) OR (created BETWEEN ? AND ?) OR " + " (deleted BETWEEN ? AND ?) OR ((created <= ?) AND (deleted >= ?))"; + // only ever touches rows created before network_id existed (never backfilled in bulk); scoped to a single + // nic so it can safely run the moment that nic is first converted to a multi-VLAN trunk nic + protected static final String BACKFILL_NETWORK_ID_FOR_NIC = + "UPDATE usage_network_offering SET network_id = ? WHERE nic_id = ? AND network_id IS NULL AND deleted IS NULL"; public UsageNetworkOfferingDaoImpl() { } @@ -59,10 +63,11 @@ public void update(UsageNetworkOfferingVO usage) { if (usage.getDeleted() != null) { try(PreparedStatement pstmt = txn.prepareStatement(UPDATE_DELETED);) { if (pstmt != null) { + // matches by primary key: the caller already identified the exact row(s) to close via + // search(); a broader (account, vm, offering) match would also close a different nic's + // still-open row for the same offering pstmt.setString(1, DateUtil.getDateDisplayString(TimeZone.getTimeZone("GMT"), usage.getDeleted())); - pstmt.setLong(2, usage.getAccountId()); - pstmt.setLong(3, usage.getVmInstanceId()); - pstmt.setLong(4, usage.getNetworkOfferingId()); + pstmt.setLong(2, usage.getId()); pstmt.executeUpdate(); } }catch (SQLException e) { @@ -120,7 +125,7 @@ public List getUsageRecords(Long accountId, Long domainI ResultSet rs = pstmt.executeQuery(); while (rs.next()) { - //zoneId, account_id, domain_id, vm_instance_id, network_offering_id, nic_id, is_default, created, deleted + //zoneId, account_id, domain_id, vm_instance_id, network_offering_id, nic_id, is_default, created, deleted, network_id Long zoneId = Long.valueOf(rs.getLong(1)); Long acctId = Long.valueOf(rs.getLong(2)); Long dId = Long.valueOf(rs.getLong(3)); @@ -132,6 +137,8 @@ public List getUsageRecords(Long accountId, Long domainI Date deletedDate = null; String createdTS = rs.getString(8); String deletedTS = rs.getString(9); + long networkIdValue = rs.getLong(10); + Long networkId = rs.wasNull() ? null : Long.valueOf(networkIdValue); if (createdTS != null) { createdDate = DateUtil.parseDateString(s_gmtTimeZone, createdTS); @@ -140,7 +147,7 @@ public List getUsageRecords(Long accountId, Long domainI deletedDate = DateUtil.parseDateString(s_gmtTimeZone, deletedTS); } - usageRecords.add(new UsageNetworkOfferingVO(zoneId, acctId, dId, vmId, noId, nicId, isDefault, createdDate, deletedDate)); + usageRecords.add(new UsageNetworkOfferingVO(zoneId, acctId, dId, vmId, noId, nicId, networkId, isDefault, createdDate, deletedDate)); } } catch (Exception e) { txn.rollback(); @@ -151,4 +158,25 @@ public List getUsageRecords(Long accountId, Long domainI return usageRecords; } + + @Override + public void backfillNetworkIdForNic(long nicId, long networkId) { + TransactionLegacy txn = TransactionLegacy.open(TransactionLegacy.USAGE_DB); + try { + txn.start(); + try (PreparedStatement pstmt = txn.prepareStatement(BACKFILL_NETWORK_ID_FOR_NIC)) { + pstmt.setLong(1, networkId); + pstmt.setLong(2, nicId); + pstmt.executeUpdate(); + } catch (SQLException e) { + throw new CloudException(String.format("Error backfilling network_id for UsageNetworkOfferingVO: %s", e.getMessage()), e); + } + txn.commit(); + } catch (Exception e) { + txn.rollback(); + logger.warn("Error backfilling network_id for UsageNetworkOfferingVO: {}", e.getMessage(), e); + } finally { + txn.close(); + } + } } 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 65946b8d8210..bcda4f787755 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 @@ protected NicVO() { @Column(name = "secondary_ip") boolean secondaryIp; + @Column(name = "multi_network") + boolean multiNetwork; + @Column(name = "mtu") Integer mtu; @@ -337,7 +340,7 @@ public String toString() { 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 void setSecondaryIp(boolean secondaryIp) { 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 000000000000..99aa445a7199 --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/vm/dao/NicNetworkMapDao.java @@ -0,0 +1,34 @@ +// 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 { + + List listByNicId(long nicId); + + List listRemovedByNicId(long nicId); + + List listByNetworkId(long networkId); + + NicNetworkMapVO findByNicIdAndNetworkId(long nicId, long networkId); + + List 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 000000000000..c8c523e95593 --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/vm/dao/NicNetworkMapDaoImpl.java @@ -0,0 +1,85 @@ +// 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 implements NicNetworkMapDao { + + private final SearchBuilder AllFieldsSearch; + private final GenericSearchBuilder 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 listByNicId(long nicId) { + SearchCriteria sc = AllFieldsSearch.create(); + sc.setParameters("nicId", nicId); + return listBy(sc); + } + + @Override + public List listRemovedByNicId(long nicId) { + SearchCriteria sc = createSearchCriteria(); + sc.addAnd("nicId", SearchCriteria.Op.EQ, nicId); + sc.addAnd("removed", SearchCriteria.Op.NNULL); + return listIncludingRemovedBy(sc); + } + + @Override + public List listByNetworkId(long networkId) { + SearchCriteria sc = AllFieldsSearch.create(); + sc.setParameters("networkId", networkId); + return listBy(sc); + } + + @Override + public NicNetworkMapVO findByNicIdAndNetworkId(long nicId, long networkId) { + SearchCriteria sc = AllFieldsSearch.create(); + sc.setParameters("nicId", nicId); + sc.setParameters("networkId", networkId); + return findOneBy(sc); + } + + @Override + public List listNicIdsByNetworkId(long networkId) { + SearchCriteria 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 000000000000..793445354606 --- /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 932db538f30b..e89d87242b22 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 @@ + 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 7c11013a17d2..e3457e508f8b 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,30 @@ --; -- 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`), + INDEX `i_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"'); + +-- Disambiguates usage_network_offering rows when a multi-VLAN trunk nic has more than one associated network +-- sharing the same network offering. NULL for rows created before this column existed - those are never +-- backfilled in bulk, only targeted, single-nic backfill on first conversion to a trunk nic. +CALL `cloud_usage`.`IDEMPOTENT_ADD_COLUMN`('cloud_usage.usage_network_offering', 'network_id', 'bigint(20) unsigned DEFAULT NULL COMMENT "associated network this usage row belongs to"'); diff --git a/engine/storage/datamotion/src/main/java/org/apache/cloudstack/storage/motion/StorageSystemDataMotionStrategy.java b/engine/storage/datamotion/src/main/java/org/apache/cloudstack/storage/motion/StorageSystemDataMotionStrategy.java index 7674f1ce25a1..99a442d77d49 100644 --- a/engine/storage/datamotion/src/main/java/org/apache/cloudstack/storage/motion/StorageSystemDataMotionStrategy.java +++ b/engine/storage/datamotion/src/main/java/org/apache/cloudstack/storage/motion/StorageSystemDataMotionStrategy.java @@ -92,6 +92,7 @@ import com.cloud.agent.api.ModifyTargetsCommand; import com.cloud.agent.api.PreMigrationCommand; import com.cloud.agent.api.PrepareForMigrationCommand; +import com.cloud.agent.api.VlanTrunkMigrationHelper; import com.cloud.agent.api.storage.CopyVolumeAnswer; import com.cloud.agent.api.storage.CopyVolumeCommand; import com.cloud.agent.api.storage.MigrateVolumeAnswer; @@ -108,6 +109,7 @@ import com.cloud.host.Host; import com.cloud.host.HostVO; import com.cloud.host.dao.HostDao; +import com.cloud.host.dao.HostDetailsDao; import com.cloud.hypervisor.Hypervisor.HypervisorType; import com.cloud.resource.ResourceState; import com.cloud.storage.DataStoreRole; @@ -145,6 +147,7 @@ import com.cloud.vm.VMInstanceVO; import com.cloud.vm.VirtualMachine; import com.cloud.vm.VirtualMachineManager; +import com.cloud.vm.dao.NicDao; import com.cloud.vm.dao.VMInstanceDao; import com.google.common.base.Preconditions; import java.util.Arrays; @@ -179,6 +182,10 @@ public class StorageSystemDataMotionStrategy implements DataMotionStrategy { @Inject private HostDao _hostDao; @Inject + private HostDetailsDao hostDetailsDao; + @Inject + private NicDao nicDao; + @Inject protected PrimaryDataStoreDao _storagePoolDao; @Inject private SnapshotDao _snapshotDao; @@ -2193,6 +2200,8 @@ public void copyAsync(Map volumeDataStoreMap, VirtualMach .anyMatch(info -> ClvmPoolManager.isClvmPoolType(info.getSourcePoolType())); migrateCommand.setClvmCrossPoolMigration(hasClvmCrossPoolVolume); + VlanTrunkMigrationHelper.populateVlanTrunkMigrationDetails(migrateCommand, (PrepareForMigrationAnswer) pfma, hostDetailsDao, destHost.getId()); + Integer newVmCpuShares = ((PrepareForMigrationAnswer) pfma).getNewVmCpuShares(); if (newVmCpuShares != null) { logger.debug(String.format("Setting CPU shares to [%d] as part of migrate VM with volumes command for VM [%s].", newVmCpuShares, vmTO)); @@ -2224,6 +2233,10 @@ public void copyAsync(Map volumeDataStoreMap, VirtualMach handlePostMigration(success, srcVolumeInfoToDestVolumeInfo, vmTO, srcHost, destHost); + if (success) { + VlanTrunkMigrationHelper.sendPostMigrationVlanTrunkMembershipIfNeeded(vmTO, destHost.getId(), nicDao, hostDetailsDao, agentManager::send, logger); + } + if (!success) { if (migrateAnswer == null) { throw new CloudRuntimeException("Unable to get an answer to the migrate command"); diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/BridgeVifDriver.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/BridgeVifDriver.java index eefe491e8b27..7f235bf6a525 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/BridgeVifDriver.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/BridgeVifDriver.java @@ -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.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -30,9 +34,14 @@ import com.cloud.utils.net.NetUtils; import com.cloud.utils.script.OutputInterpreter; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; import org.apache.commons.lang3.StringUtils; +import org.libvirt.Domain; 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; @@ -199,6 +208,210 @@ protected boolean isValidProtocolAndVnetId(final String vNetId, final String pro return vNetId != null && protocol != null && !vNetId.equalsIgnoreCase("untagged"); } + protected boolean usesSharedVlanAwareBridge(final NicTO nic) { + return nic.getBroadcastType() == Networks.BroadcastDomainType.Vlan && _libvirtComputingResource.hostSupportsVlanFiltering(); + } + + 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"); + + Integer primaryVlanTag = parseVlanTag(nic.getBroadcastUri(), "primary network of nic " + nic.getMac()); + List vlanTags = collectTrunkVlanTags(nic); + warnIfUplinkMissingVlanMembership(brName, vlanTags); + + logger.debug("plugging trunk nic " + nic.getMac() + " onto guest bridge " + brName + " with vlan tags " + vlanTags + + ", native vlan " + primaryVlanTag); + intf.defBridgeNet(brName, null, nic.getMac(), getGuestNicModel(guestOsType, nicAdapter), networkRateKBps); + + if (_libvirtComputingResource.hostSupportsVlanTrunkXml()) { + intf.setTrunkVlanTags(vlanTags, primaryVlanTag); + } + // else: older libvirt can't express trunk membership; ensureVlanTrunkMembership() applies it manually once the tap exists + } + + private List collectTrunkVlanTags(NicTO nic) throws InternalErrorException { + Set 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); + } + } + + /** + * The uplink's own tagged VLAN membership is the operator's responsibility, not CloudStack's - the uplink + * is never modified here. This only gives the operator a diagnostic signal: a VLAN missing from the uplink + * still lets same-host traffic on that VLAN work (pure bridge-local forwarding never touches the uplink), + * while cross-host traffic on it silently fails - a confusing signature this warning is meant to shortcut. + * Never blocks the plug and never throws; a failure to even read the uplink's membership is itself just logged. + */ + private void warnIfUplinkMissingVlanMembership(String brName, List vlanTags) { + String uplinkPif = _pifs.get(brName); + if (StringUtils.isBlank(uplinkPif)) { + logger.warn("Cannot determine the uplink interface for guest bridge {} to check VLAN membership for tags {}; " + + "cross-host traffic for this nic will fail unless the uplink has already been configured for these VLANs", brName, vlanTags); + return; + } + try { + Map currentMembership = readCurrentVlanMembership(uplinkPif); + List missingTags = new ArrayList<>(); + for (Integer vlanTag : vlanTags) { + if (!currentMembership.containsKey(vlanTag)) { + missingTags.add(vlanTag); + } + } + if (!missingTags.isEmpty()) { + logger.warn("Uplink {} is not a tagged member of VLAN(s) {}; same-host traffic on these VLANs will work, but cross-host " + + "traffic will not, until an operator adds them (e.g. 'bridge vlan add dev {} vid ')", uplinkPif, missingTags, uplinkPif); + } + } catch (InternalErrorException e) { + logger.warn("Unable to check VLAN membership on uplink {}: {}", uplinkPif, e.getMessage()); + } + } + + @Override + public void ensureVlanTrunkMembership(LibvirtVMDef.InterfaceDef iface, NicTO nic) throws InternalErrorException { + boolean usesSharedBridge = nic.isTrunkVlan() || usesSharedVlanAwareBridge(nic); + if (!usesSharedBridge || _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()); + } + Integer primaryVlanTag = parseVlanTag(nic.getBroadcastUri(), "primary network of nic " + nic.getMac()); + for (Integer vlanTag : collectTrunkVlanTags(nic)) { + runBridgeVlanCommand("add", tapName, String.valueOf(vlanTag), vlanTag.equals(primaryVlanTag)); + } + } + + protected void runBridgeVlanCommand(String operation, String dev, String vid, boolean pvidUntagged) throws InternalErrorException { + final Script command = new Script("bridge", _timeout, logger); + command.add("vlan"); + command.add(operation); + command.add("dev", dev); + command.add("vid", vid); + if (pvidUntagged) { + command.add("pvid"); + command.add("untagged"); + } + final String result = command.execute(); + if (result != null) { + throw new InternalErrorException("Failed to " + operation + " VLAN " + vid + " membership on " + dev + ": " + result); + } + } + + @Override + public void updateVlanTrunkMembership(Domain vm, LibvirtVMDef.InterfaceDef iface, NicTO nic) throws InternalErrorException, LibvirtException { + boolean usesSharedBridge = nic.isTrunkVlan() || usesSharedVlanAwareBridge(nic); + if (!usesSharedBridge) { + throw new InternalErrorException("Nic " + nic.getMac() + " does not use the shared VLAN-aware bridge; cannot update its VLAN membership live"); + } + if (!_libvirtComputingResource.hostSupportsVlanFiltering()) { + throw new InternalErrorException("vlan_filtering is not enabled on this host's guest bridge; cannot update multi-VLAN trunk membership live"); + } + + Integer primaryVlanTag = parseVlanTag(nic.getBroadcastUri(), "primary network of nic " + nic.getMac()); + List vlanTags = collectTrunkVlanTags(nic); + + if (_libvirtComputingResource.hostSupportsVlanTrunkXml()) { + iface.setTrunkVlanTags(vlanTags, primaryVlanTag); + // CloudStack's KVM domains are transient (no persistent libvirt config to update) - matches the + // existing LIVE-only precedent in LibvirtReplugNicCommandWrapper/LibvirtUpdateVmNicCommandWrapper. + // Persistence across a restart is already guaranteed by nic_network_map/nics.network_id, not libvirt. + vm.updateDeviceFlags(iface.toString(), Domain.DeviceModifyFlags.LIVE); + } else { + applyVlanTrunkMembershipDiff(iface.getDevName(), vlanTags, primaryVlanTag); + } + } + + private void applyVlanTrunkMembershipDiff(String tapName, List desiredVlanTags, Integer primaryVlanTag) throws InternalErrorException { + if (StringUtils.isBlank(tapName)) { + throw new InternalErrorException("Cannot update VLAN trunk membership: tap device name unknown"); + } + Map currentMembership = readCurrentVlanMembership(tapName); + Set desiredSet = new LinkedHashSet<>(desiredVlanTags); + + for (Integer currentVlan : currentMembership.keySet()) { + if (!desiredSet.contains(currentVlan)) { + runBridgeVlanCommand("del", tapName, String.valueOf(currentVlan), false); + } + } + for (Integer desiredVlan : desiredSet) { + boolean shouldBePvid = desiredVlan.equals(primaryVlanTag); + Boolean currentlyPvid = currentMembership.get(desiredVlan); + if (currentlyPvid == null || currentlyPvid != shouldBePvid) { + runBridgeVlanCommand("add", tapName, String.valueOf(desiredVlan), shouldBePvid); + } + } + } + + protected Map readCurrentVlanMembership(String tapName) throws InternalErrorException { + final Script command = new Script("bridge", _timeout, logger); + command.add("-j"); + command.add("vlan"); + command.add("show"); + command.add("dev", tapName); + final OutputInterpreter.AllLinesParser parser = new OutputInterpreter.AllLinesParser(); + final String errors = command.execute(parser); + if (errors != null) { + throw new InternalErrorException("Failed to read current VLAN membership for " + tapName + ": " + errors); + } + + final Map membership = new LinkedHashMap<>(); + final String json = parser.getLines(); + if (StringUtils.isBlank(json)) { + return membership; + } + for (JsonElement deviceEl : JsonParser.parseString(json).getAsJsonArray()) { + JsonObject device = deviceEl.getAsJsonObject(); + if (!device.has("vlans")) { + continue; + } + for (JsonElement vlanEl : device.getAsJsonArray("vlans")) { + JsonObject vlanObj = vlanEl.getAsJsonObject(); + int vlan = vlanObj.get("vlan").getAsInt(); + boolean pvid = false; + if (vlanObj.has("flags")) { + for (JsonElement flagEl : vlanObj.getAsJsonArray("flags")) { + if ("PVID".equalsIgnoreCase(flagEl.getAsString())) { + pvid = true; + } + } + } + membership.put(vlan, pvid); + } + } + return membership; + } + protected String createStorageVnetBridgeIfNeeded(NicTO nic, String trafficLabel, String storageBrName) throws InternalErrorException { if (nic.getBroadcastUri() == null) { @@ -248,8 +461,15 @@ public LibvirtVMDef.InterfaceDef plug(NicTO nic, String guestOsType, String nicA } if (nic.getType() == Networks.TrafficType.Guest) { - if (isBroadcastTypeVlanOrVxlan(nic) && isValidProtocolAndVnetId(vNetId, protocol)) { - if (trafficLabel != null && !trafficLabel.isEmpty()) { + if (nic.isTrunkVlan()) { + plugTrunkVlanNic(intf, nic, trafficLabel, guestOsType, nicAdapter, networkRateKBps); + } else if (isBroadcastTypeVlanOrVxlan(nic) && isValidProtocolAndVnetId(vNetId, protocol)) { + if (usesSharedVlanAwareBridge(nic)) { + // host is VLAN-filtering-ready: single-VLAN nics share the same VLAN-aware bridge + // trunk nics use, rather than getting their own dedicated per-VLAN bridge, so the + // two mechanisms never end up on disconnected bridges for the same VLAN on one host + plugTrunkVlanNic(intf, nic, trafficLabel, guestOsType, nicAdapter, networkRateKBps); + } else 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); intf.defBridgeNet(brName, null, nic.getMac(), getGuestNicModel(guestOsType, nicAdapter), networkRateKBps); 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 9009ec629ca3..50898b59ddfc 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 @@ -25,6 +25,8 @@ 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; @@ -357,6 +359,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 (<vlan trunk='yes'>) 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. */ @@ -4437,6 +4443,8 @@ public StartupCommand[] initialize() { 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); } @@ -6268,6 +6276,39 @@ public boolean hostSupportsInstanceConversion() { return exitValue == 0; } + /** + * Static capability: whether this host's libvirt understands multi-tag trunk vlan XML + * (<vlan trunk='yes'>) 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/LibvirtVMDef.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtVMDef.java index 439e4f663416..414545041f63 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtVMDef.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtVMDef.java @@ -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; @@ -1613,6 +1614,9 @@ enum HostNicType { private String _virtualPortType; private String _virtualPortInterfaceId; private int _vlanTag = -1; + private boolean _vlanTrunk = false; + private List _vlanTrunkTags; + private Integer _nativeVlanTag; private boolean _pxeDisable = false; private boolean _linkStateUp = true; private Integer _slot; @@ -1773,6 +1777,24 @@ public int getVlanTag() { return _vlanTag; } + public void setTrunkVlanTags(List vlanTags, Integer nativeVlanTag) { + _vlanTrunk = true; + _vlanTrunkTags = vlanTags; + _nativeVlanTag = nativeVlanTag; + } + + public List getTrunkVlanTags() { + return _vlanTrunkTags; + } + + public Integer getNativeVlanTag() { + return _nativeVlanTag; + } + + public boolean isVlanTrunk() { + return _vlanTrunk; + } + public void setSlot(Integer slot) { _slot = slot; } @@ -1876,7 +1898,17 @@ public String getContent() { } netBuilder.append("\n"); } - if (_vlanTag > 0 && _vlanTag < 4095) { + if (_vlanTrunk && CollectionUtils.isNotEmpty(_vlanTrunkTags)) { + netBuilder.append("\n"); + for (Integer tag : _vlanTrunkTags) { + if (tag.equals(_nativeVlanTag)) { + netBuilder.append("\n"); + } else { + netBuilder.append("\n"); + } + } + netBuilder.append(""); + } else if (_vlanTag > 0 && _vlanTag < 4095) { netBuilder.append("\n\n"); } diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/VifDriver.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/VifDriver.java index 72fb82967814..a09a30916220 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/VifDriver.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/VifDriver.java @@ -23,6 +23,7 @@ import javax.naming.ConfigurationException; +import org.libvirt.Domain; import org.libvirt.LibvirtException; import com.cloud.agent.api.to.NicTO; @@ -46,4 +47,13 @@ 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 XML; no-op otherwise + default void ensureVlanTrunkMembership(LibvirtVMDef.InterfaceDef iface, NicTO nic) throws InternalErrorException { + } + + // applies nic's full desired VLAN membership (primary + associated networks) to an already-plugged trunk nic on a running Instance + default void updateVlanTrunkMembership(Domain vm, LibvirtVMDef.InterfaceDef iface, NicTO nic) throws InternalErrorException, LibvirtException { + throw new InternalErrorException("This vif driver does not support live multi-VLAN trunk membership updates"); + } + } diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtMigrateCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtMigrateCommandWrapper.java index ed02ae6da38d..2396227bc495 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtMigrateCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtMigrateCommandWrapper.java @@ -19,6 +19,7 @@ import java.io.IOException; import java.io.InputStream; +import java.net.URI; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; @@ -76,6 +77,8 @@ import com.cloud.agent.api.MigrateCommand.MigrateDiskInfo; import com.cloud.agent.api.to.DiskTO; import com.cloud.agent.api.to.DpdkTO; +import com.cloud.agent.api.to.NetworkTO; +import com.cloud.agent.api.to.NicTO; import com.cloud.agent.api.to.VirtualMachineTO; import com.cloud.agent.properties.AgentProperties; import com.cloud.agent.properties.AgentPropertiesFileHandler; @@ -86,6 +89,7 @@ import com.cloud.hypervisor.kvm.resource.LibvirtVMDef.InterfaceDef; import com.cloud.hypervisor.kvm.resource.MigrateKVMAsync; import com.cloud.hypervisor.kvm.resource.VifDriver; +import com.cloud.network.Networks; @ResourceWrapper(handles = MigrateCommand.class) public final class LibvirtMigrateCommandWrapper extends CommandWrapper { @@ -229,6 +233,8 @@ Use VIR_DOMAIN_XML_SECURE (value = 1) prior to v1.0.0. } } + xmlDesc = replaceVlanTrunkInterfaces(xmlDesc, command, libvirtComputingResource); + xmlDesc = updateVmSharesIfNeeded(command, xmlDesc, libvirtComputingResource); xmlDesc = updateGpuDevicesIfNeeded(command, xmlDesc, libvirtComputingResource); @@ -688,6 +694,152 @@ protected String replaceDpdkInterfaces(String xmlDesc, Map dpdkP return LibvirtXMLParser.getXml(doc); } + // Rewrites Vlan-broadcast nics' bridge/trunk XML for the destination's capability; fails closed if that capability is unknown. + protected String replaceVlanTrunkInterfaces(String xmlDesc, MigrateCommand command, LibvirtComputingResource libvirtComputingResource) + throws ParserConfigurationException, IOException, SAXException, TransformerException { + NicTO[] nics = command.getVirtualMachine().getNics(); + boolean sourceVlanFilteringEnabled = libvirtComputingResource.hostSupportsVlanFiltering(); + // destination-shared-bridge is also a rewrite trigger: a legacy-source nic's bridge name (breth1-) + // doesn't exist on a shared-bridge destination, needs rewriting to cloudbr1 just as much as the reverse + boolean destVlanFilteringEnabled = Boolean.TRUE.equals(command.getDestVlanFilteringEnabled()); + boolean anyNicNeedsHandling = false; + for (NicTO nic : nics) { + if (nic.getBroadcastType() == Networks.BroadcastDomainType.Vlan && (sourceVlanFilteringEnabled || destVlanFilteringEnabled || nic.isTrunkVlan())) { + anyNicNeedsHandling = true; + break; + } + } + if (!anyNicNeedsHandling) { + return xmlDesc; + } + + InputStream in = IOUtils.toInputStream(xmlDesc); + DocumentBuilderFactory docFactory = ParserUtils.getSaferDocumentBuilderFactory(); + DocumentBuilder docBuilder = docFactory.newDocumentBuilder(); + Document doc = docBuilder.parse(in); + + Node domainNode = doc.getFirstChild(); + NodeList domainChildNodes = domainNode.getChildNodes(); + for (int i = 0; i < domainChildNodes.getLength(); i++) { + Node domainChildNode = domainChildNodes.item(i); + if (!"devices".equals(domainChildNode.getNodeName())) { + continue; + } + NodeList devicesChildNodes = domainChildNode.getChildNodes(); + for (int x = 0; x < devicesChildNodes.getLength(); x++) { + Node deviceChildNode = devicesChildNodes.item(x); + if (!"interface".equals(deviceChildNode.getNodeName()) || !(deviceChildNode instanceof Element)) { + continue; + } + Element interfaceElement = (Element) deviceChildNode; + String mac = findInterfaceMacAddress(interfaceElement); + NicTO nic = mac == null ? null : findNicByMac(nics, mac); + if (nic == null || nic.getBroadcastType() != Networks.BroadcastDomainType.Vlan + || !(sourceVlanFilteringEnabled || destVlanFilteringEnabled || nic.isTrunkVlan())) { + continue; + } + replaceVlanTrunkInterface(doc, interfaceElement, nic, command); + } + } + + return LibvirtXMLParser.getXml(doc); + } + + private NicTO findNicByMac(NicTO[] nics, String mac) { + for (NicTO nic : nics) { + if (mac.equalsIgnoreCase(nic.getMac())) { + return nic; + } + } + return null; + } + + private String findInterfaceMacAddress(Element interfaceElement) { + NodeList children = interfaceElement.getChildNodes(); + for (int i = 0; i < children.getLength(); i++) { + Node child = children.item(i); + if ("mac".equals(child.getNodeName()) && child.getAttributes() != null) { + Node addressAttr = child.getAttributes().getNamedItem("address"); + if (addressAttr != null) { + return addressAttr.getNodeValue(); + } + } + } + return null; + } + + private void replaceVlanTrunkInterface(Document doc, Element interfaceElement, NicTO nic, MigrateCommand command) { + String destBridge = command.getNicBridgeMapping().get(nic.getMac()); + Boolean destVlanFilteringEnabled = command.getDestVlanFilteringEnabled(); + Boolean destVlanTrunkXmlSupported = command.getDestVlanTrunkXmlSupported(); + if (StringUtils.isBlank(destBridge) || destVlanFilteringEnabled == null || destVlanTrunkXmlSupported == null) { + throw new CloudRuntimeException(String.format( + "Refusing to migrate nic %s: destination host's multi-VLAN capability is unknown to this migration command", nic.getMac())); + } + + NodeList children = interfaceElement.getChildNodes(); + for (int i = 0; i < children.getLength(); i++) { + Node child = children.item(i); + if ("source".equals(child.getNodeName()) && child.getAttributes() != null) { + Node bridgeAttr = child.getAttributes().getNamedItem("bridge"); + if (bridgeAttr != null) { + bridgeAttr.setNodeValue(destBridge); + } + } + } + + Node existingVlanNode = null; + for (int i = 0; i < children.getLength(); i++) { + if ("vlan".equals(children.item(i).getNodeName())) { + existingVlanNode = children.item(i); + break; + } + } + if (existingVlanNode != null) { + interfaceElement.removeChild(existingVlanNode); + } + + if (destVlanTrunkXmlSupported) { + interfaceElement.appendChild(buildVlanTrunkElement(doc, nic)); + } + // else: membership is applied manually on the destination tap instead (ensureVlanTrunkMembership) + } + + private Element buildVlanTrunkElement(Document doc, NicTO nic) { + Element vlanElement = doc.createElement("vlan"); + vlanElement.setAttribute("trunk", "yes"); + + Integer primaryTag = parseVlanTag(nic.getBroadcastUri()); + appendVlanTagElement(doc, vlanElement, primaryTag, true); + if (nic.getAssociatedNetworks() != null) { + for (NetworkTO associatedNetwork : nic.getAssociatedNetworks()) { + appendVlanTagElement(doc, vlanElement, parseVlanTag(associatedNetwork.getBroadcastUri()), false); + } + } + return vlanElement; + } + + private void appendVlanTagElement(Document doc, Element vlanElement, Integer tag, boolean nativeUntagged) { + Element tagElement = doc.createElement("tag"); + tagElement.setAttribute("id", String.valueOf(tag)); + if (nativeUntagged) { + tagElement.setAttribute("nativeMode", "untagged"); + } + vlanElement.appendChild(tagElement); + } + + private Integer parseVlanTag(URI broadcastUri) { + String vlanValue = broadcastUri == null ? null : Networks.BroadcastDomainType.getValue(broadcastUri); + if (StringUtils.isBlank(vlanValue)) { + throw new CloudRuntimeException("Cannot determine VLAN from broadcast URI " + broadcastUri + " while preparing migration XML"); + } + try { + return Integer.valueOf(vlanValue); + } catch (NumberFormatException e) { + throw new CloudRuntimeException("Invalid VLAN value '" + vlanValue + "' in broadcast URI " + broadcastUri); + } + } + /** * In case of a local file, it deletes the file on the source host/storage pool. Otherwise (for instance iScsi) it disconnects the disk on the source storage pool.
* This method must be executed after a successful migration to a target storage pool, cleaning up the source storage. diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtPlugNicCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtPlugNicCommandWrapper.java index b0950376a93d..b1bc65ec73ce 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtPlugNicCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtPlugNicCommandWrapper.java @@ -27,8 +27,10 @@ import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; import com.cloud.hypervisor.kvm.resource.LibvirtVMDef.InterfaceDef; import com.cloud.hypervisor.kvm.resource.VifDriver; +import com.cloud.network.Networks.BroadcastDomainType; 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; @@ -67,6 +69,15 @@ public Answer execute(final PlugNicCommand command, final LibvirtComputingResour } vm.attachDevice(interfaceDef.toString()); + if (nic.getBroadcastType() == BroadcastDomainType.Vlan) { + try { + 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]); diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtPostMigrationCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtPostMigrationCommandWrapper.java index 608770974dc1..2c267179274b 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtPostMigrationCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtPostMigrationCommandWrapper.java @@ -29,10 +29,14 @@ import com.cloud.agent.api.Answer; import com.cloud.agent.api.PostMigrationAnswer; import com.cloud.agent.api.PostMigrationCommand; +import com.cloud.agent.api.to.NicTO; import com.cloud.agent.api.to.VirtualMachineTO; +import com.cloud.exception.InternalErrorException; import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; import com.cloud.hypervisor.kvm.resource.LibvirtConnection; +import com.cloud.hypervisor.kvm.resource.LibvirtVMDef; import com.cloud.hypervisor.kvm.resource.LibvirtVMDef.DiskDef; +import com.cloud.network.Networks; import com.cloud.resource.CommandWrapper; import com.cloud.resource.ResourceWrapper; @@ -68,6 +72,8 @@ public Answer execute(final PostMigrationCommand command, final LibvirtComputing LibvirtComputingResource.ClvmVolumeState.EXCLUSIVE ); + applyManualVlanTrunkMembership(conn, vmName, vm.getNics(), libvirtComputingResource); + logger.debug("Successfully completed post-migration tasks for VM {}", vmName); return new PostMigrationAnswer(command); @@ -79,4 +85,18 @@ public Answer execute(final PostMigrationCommand command, final LibvirtComputing return new PostMigrationAnswer(command, e); } } + + private void applyManualVlanTrunkMembership(Connect conn, String vmName, NicTO[] nics, LibvirtComputingResource libvirtComputingResource) + throws InternalErrorException { + if (nics == null) { + return; + } + for (NicTO nic : nics) { + if (nic.getBroadcastType() != Networks.BroadcastDomainType.Vlan) { + continue; + } + LibvirtVMDef.InterfaceDef liveInterface = libvirtComputingResource.getInterface(conn, vmName, nic.getMac()); + libvirtComputingResource.getVifDriver(nic.getType(), nic.getName()).ensureVlanTrunkMembership(liveInterface, nic); + } + } } diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtPrepareForMigrationCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtPrepareForMigrationCommandWrapper.java index f7ca79127dad..7d02c728b526 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtPrepareForMigrationCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtPrepareForMigrationCommandWrapper.java @@ -42,7 +42,9 @@ import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; import com.cloud.hypervisor.kvm.resource.LibvirtVMDef; import com.cloud.hypervisor.kvm.resource.LibvirtVMDef.InterfaceDef.GuestNetType; +import com.cloud.hypervisor.kvm.resource.VifDriver; import com.cloud.hypervisor.kvm.storage.KVMStoragePoolManager; +import com.cloud.network.Networks; import com.cloud.resource.CommandWrapper; import com.cloud.resource.ResourceWrapper; import com.cloud.storage.Volume; @@ -68,6 +70,7 @@ public Answer execute(final PrepareForMigrationCommand command, final LibvirtCom final NicTO[] nics = vm.getNics(); Map dpdkInterfaceMapping = new HashMap<>(); + Map nicBridgeMapping = new HashMap<>(); boolean skipDisconnect = false; @@ -78,7 +81,8 @@ public Answer execute(final PrepareForMigrationCommand command, final LibvirtCom final Connect conn = libvirtUtilitiesHelper.getConnectionByVmName(vm.getName()); for (final NicTO nic : nics) { - LibvirtVMDef.InterfaceDef interfaceDef = libvirtComputingResource.getVifDriver(nic.getType(), nic.getName()).plug(nic, null, "", vm.getExtraConfig()); + VifDriver vifDriver = libvirtComputingResource.getVifDriver(nic.getType(), nic.getName()); + LibvirtVMDef.InterfaceDef interfaceDef = vifDriver.plug(nic, null, "", vm.getExtraConfig()); if (vm.getDetails() != null) { libvirtComputingResource.setInterfaceDefQueueSettings(vm.getDetails(), vm.getCpus(), interfaceDef); } @@ -87,6 +91,9 @@ public Answer execute(final PrepareForMigrationCommand command, final LibvirtCom dpdkInterfaceMapping.put(nic.getMac(), to); logger.debug("Configured DPDK interface for VM {}", vm.getName()); } + if (nic.getBroadcastType() == Networks.BroadcastDomainType.Vlan && interfaceDef != null) { + nicBridgeMapping.put(nic.getMac(), interfaceDef.getBrName()); + } } /* setup disks, e.g for iso */ @@ -139,7 +146,7 @@ public Answer execute(final PrepareForMigrationCommand command, final LibvirtCom } logger.info("Successfully prepared destination host for migration of VM {}", vm.getName()); - return createPrepareForMigrationAnswer(command, dpdkInterfaceMapping, libvirtComputingResource, vm); + return createPrepareForMigrationAnswer(command, dpdkInterfaceMapping, nicBridgeMapping, libvirtComputingResource, vm); } catch (final LibvirtException | CloudRuntimeException | InternalErrorException | URISyntaxException e) { if (MapUtils.isNotEmpty(dpdkInterfaceMapping)) { for (DpdkTO to : dpdkInterfaceMapping.values()) { @@ -155,6 +162,7 @@ public Answer execute(final PrepareForMigrationCommand command, final LibvirtCom } protected PrepareForMigrationAnswer createPrepareForMigrationAnswer(PrepareForMigrationCommand command, Map dpdkInterfaceMapping, + Map nicBridgeMapping, LibvirtComputingResource libvirtComputingResource, VirtualMachineTO vm) { PrepareForMigrationAnswer answer = new PrepareForMigrationAnswer(command); @@ -163,6 +171,10 @@ protected PrepareForMigrationAnswer createPrepareForMigrationAnswer(PrepareForMi answer.setDpdkInterfaceMapping(dpdkInterfaceMapping); } + if (MapUtils.isNotEmpty(nicBridgeMapping)) { + answer.setNicBridgeMapping(nicBridgeMapping); + } + int newCpuShares = libvirtComputingResource.calculateCpuShares(vm); logger.debug(String.format("Setting CPU shares to [%s] for the migration of VM [%s].", newCpuShares, vm)); answer.setNewVmCpuShares(newCpuShares); 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 1ff6d7851f20..94af18b71a3b 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 @@ -60,6 +60,9 @@ public Answer execute(final ReadyCommand command, final LibvirtComputingResource 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/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtStartCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtStartCommandWrapper.java index 486989661909..fd771add7bea 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtStartCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtStartCommandWrapper.java @@ -46,9 +46,11 @@ import com.cloud.hypervisor.kvm.resource.LibvirtKvmAgentHook; import com.cloud.hypervisor.kvm.resource.LibvirtVMDef; import com.cloud.hypervisor.kvm.storage.KVMStoragePoolManager; +import com.cloud.network.Networks.BroadcastDomainType; 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; @@ -96,6 +98,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); @@ -179,6 +182,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.getBroadcastType() != BroadcastDomainType.Vlan) { + 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 secondaryStorages) { if (CollectionUtils.isNotEmpty(command.getSecondaryStorages())) { for (String secondaryStorageUrl : command.getSecondaryStorages()) { diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtUpdateNicVlanMembershipCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtUpdateNicVlanMembershipCommandWrapper.java new file mode 100644 index 000000000000..83c81deb005e --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtUpdateNicVlanMembershipCommandWrapper.java @@ -0,0 +1,71 @@ +// +// 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.hypervisor.kvm.resource.wrapper; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.UpdateNicVlanMembershipAnswer; +import com.cloud.agent.api.UpdateNicVlanMembershipCommand; +import com.cloud.agent.api.to.NicTO; +import com.cloud.exception.InternalErrorException; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.hypervisor.kvm.resource.LibvirtVMDef.InterfaceDef; +import com.cloud.hypervisor.kvm.resource.VifDriver; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ResourceWrapper; +import com.cloud.utils.exception.CloudRuntimeException; +import org.libvirt.Connect; +import org.libvirt.Domain; +import org.libvirt.LibvirtException; + +@ResourceWrapper(handles = UpdateNicVlanMembershipCommand.class) +public final class LibvirtUpdateNicVlanMembershipCommandWrapper + extends CommandWrapper { + + @Override + public Answer execute(final UpdateNicVlanMembershipCommand command, final LibvirtComputingResource libvirtComputingResource) { + final NicTO nic = command.getNic(); + final String vmName = command.getVmName(); + Domain vm = null; + try { + final LibvirtUtilitiesHelper libvirtUtilitiesHelper = libvirtComputingResource.getLibvirtUtilitiesHelper(); + final Connect conn = libvirtUtilitiesHelper.getConnectionByVmName(vmName); + vm = libvirtComputingResource.getDomain(conn, vmName); + + final InterfaceDef iface = libvirtComputingResource.getInterface(conn, vmName, nic.getMac()); + + final VifDriver vifDriver = libvirtComputingResource.getVifDriver(nic.getType(), nic.getName()); + vifDriver.updateVlanTrunkMembership(vm, iface, nic); + + return new UpdateNicVlanMembershipAnswer(command, true, "success"); + } catch (final LibvirtException | InternalErrorException | CloudRuntimeException e) { + final String msg = "Update NIC VLAN membership failed due to " + e; + logger.warn(msg, e); + return new UpdateNicVlanMembershipAnswer(command, false, msg); + } finally { + if (vm != null) { + try { + vm.free(); + } catch (final LibvirtException l) { + logger.trace("Ignoring libvirt error.", l); + } + } + } + } +} diff --git a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/BridgeVifDriverTest.java b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/BridgeVifDriverTest.java index 00364948f828..e5e092d3aa09 100644 --- a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/BridgeVifDriverTest.java +++ b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/BridgeVifDriverTest.java @@ -18,15 +18,22 @@ import java.net.URI; import java.net.URISyntaxException; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import org.junit.Assert; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; +import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.Spy; import org.mockito.junit.MockitoJUnitRunner; +import com.cloud.agent.api.to.NetworkTO; import com.cloud.agent.api.to.NicTO; import com.cloud.exception.InternalErrorException; import com.cloud.network.Networks; @@ -36,10 +43,27 @@ public class BridgeVifDriverTest { private static final String BRIDGE_NAME = "cloudbr1"; + @Mock + private LibvirtComputingResource libvirtComputingResource; + @Spy @InjectMocks private BridgeVifDriver driver = new BridgeVifDriver(); + @Before + public void setup() throws InternalErrorException { + driver._libvirtComputingResource = libvirtComputingResource; + Map bridges = new HashMap<>(); + bridges.put("guest", BRIDGE_NAME); + driver._bridges = bridges; + Map pifs = new HashMap<>(); + pifs.put("private", "eth1"); + pifs.put(BRIDGE_NAME, "eth1"); + pifs.put("customLabel", "eth2"); + driver._pifs = pifs; + Mockito.lenient().doNothing().when(driver).runBridgeVlanCommand(Mockito.anyString(), Mockito.anyString(), Mockito.anyString(), Mockito.anyBoolean()); + } + @Test public void isBroadcastTypeVlanOrVxlan() { final NicTO nic = new NicTO(); @@ -100,4 +124,295 @@ public void createStorageVnetBridgeIfNeededCreatesVnetBridgeWhenUntaggedVlan() t String result = driver.createStorageVnetBridgeIfNeeded(nic, "trafficLabel", BRIDGE_NAME); Assert.assertEquals(BRIDGE_NAME, result); } + + private NicTO buildTrunkNic(int primaryVlan, List associatedVlans) { + NicTO nic = new NicTO(); + nic.setBroadcastType(Networks.BroadcastDomainType.Vlan); + nic.setBroadcastUri(Networks.BroadcastDomainType.Vlan.toUri(primaryVlan)); + nic.setMac("00:11:22:aa:bb:dd"); + nic.setTrunkVlan(true); + if (associatedVlans != null) { + List associated = new java.util.ArrayList<>(); + for (Integer vlan : associatedVlans) { + NetworkTO associatedTo = new NetworkTO(); + associatedTo.setBroadcastType(Networks.BroadcastDomainType.Vlan); + associatedTo.setBroadcastUri(Networks.BroadcastDomainType.Vlan.toUri(vlan)); + associated.add(associatedTo); + } + nic.setAssociatedNetworks(associated); + } + return nic; + } + + @Test + public void plugTrunkVlanNicOnOldLibvirtSkipsXmlAndLeavesManualMembershipToPostAttachHook() throws InternalErrorException { + Mockito.when(libvirtComputingResource.hostSupportsVlanTrunkXml()).thenReturn(false); + Mockito.when(libvirtComputingResource.hostSupportsVlanFiltering()).thenReturn(true); + NicTO nic = buildTrunkNic(100, Collections.singletonList(200)); + LibvirtVMDef.InterfaceDef intf = new LibvirtVMDef.InterfaceDef(); + + driver.plugTrunkVlanNic(intf, nic, null, null, null, 0); + + Assert.assertFalse(intf.isVlanTrunk()); + Assert.assertEquals(BRIDGE_NAME, intf.getBrName()); + } + + @Test(expected = InternalErrorException.class) + public void plugTrunkVlanNicFailsWhenVlanFilteringNotEnabled() throws InternalErrorException { + Mockito.when(libvirtComputingResource.hostSupportsVlanFiltering()).thenReturn(false); + NicTO nic = buildTrunkNic(100, Collections.singletonList(200)); + driver.plugTrunkVlanNic(new LibvirtVMDef.InterfaceDef(), nic, null, null, null, 0); + } + + @Test + public void ensureVlanTrunkMembershipNoOpsWhenHostSupportsTrunkXml() throws InternalErrorException { + Mockito.when(libvirtComputingResource.hostSupportsVlanTrunkXml()).thenReturn(true); + NicTO nic = buildTrunkNic(100, Collections.singletonList(200)); + LibvirtVMDef.InterfaceDef intf = new LibvirtVMDef.InterfaceDef(); + intf.setDevName("vnet5"); + + driver.ensureVlanTrunkMembership(intf, nic); + + Mockito.verify(driver, Mockito.never()).runBridgeVlanCommand(Mockito.anyString(), Mockito.anyString(), Mockito.anyString(), Mockito.anyBoolean()); + } + + @Test + public void ensureVlanTrunkMembershipNoOpsForNonTrunkNic() throws InternalErrorException { + NicTO nic = new NicTO(); + nic.setTrunkVlan(false); + LibvirtVMDef.InterfaceDef intf = new LibvirtVMDef.InterfaceDef(); + intf.setDevName("vnet5"); + + driver.ensureVlanTrunkMembership(intf, nic); + + Mockito.verify(driver, Mockito.never()).runBridgeVlanCommand(Mockito.anyString(), Mockito.anyString(), Mockito.anyString(), Mockito.anyBoolean()); + } + + @Test + public void usesSharedVlanAwareBridgeTrueOnlyForVlanNicOnFilteringReadyHost() { + NicTO nic = new NicTO(); + nic.setBroadcastType(Networks.BroadcastDomainType.Vlan); + + Mockito.when(libvirtComputingResource.hostSupportsVlanFiltering()).thenReturn(true); + Assert.assertTrue(driver.usesSharedVlanAwareBridge(nic)); + + Mockito.when(libvirtComputingResource.hostSupportsVlanFiltering()).thenReturn(false); + Assert.assertFalse(driver.usesSharedVlanAwareBridge(nic)); + + nic.setBroadcastType(Networks.BroadcastDomainType.Vxlan); + Assert.assertFalse(driver.usesSharedVlanAwareBridge(nic)); + } + + @Test + public void ensureVlanTrunkMembershipAppliesNativeVlanForOrdinaryNicOnFilteringReadyHost() throws InternalErrorException { + Mockito.when(libvirtComputingResource.hostSupportsVlanTrunkXml()).thenReturn(false); + Mockito.when(libvirtComputingResource.hostSupportsVlanFiltering()).thenReturn(true); + NicTO nic = buildTrunkNic(100, null); + nic.setTrunkVlan(false); + LibvirtVMDef.InterfaceDef intf = new LibvirtVMDef.InterfaceDef(); + intf.setDevName("vnet5"); + + driver.ensureVlanTrunkMembership(intf, nic); + + Mockito.verify(driver).runBridgeVlanCommand("add", "vnet5", "100", true); + } + + @Test + public void ensureVlanTrunkMembershipNoOpsForOrdinaryNicWhenHostNotFilteringReady() throws InternalErrorException { + Mockito.when(libvirtComputingResource.hostSupportsVlanFiltering()).thenReturn(false); + NicTO nic = buildTrunkNic(100, null); + nic.setTrunkVlan(false); + LibvirtVMDef.InterfaceDef intf = new LibvirtVMDef.InterfaceDef(); + intf.setDevName("vnet5"); + + driver.ensureVlanTrunkMembership(intf, nic); + + Mockito.verify(driver, Mockito.never()).runBridgeVlanCommand(Mockito.anyString(), Mockito.anyString(), Mockito.anyString(), Mockito.anyBoolean()); + } + + @Test + public void ensureVlanTrunkMembershipAppliesManualBridgeVlanAddOnOldLibvirt() throws InternalErrorException { + Mockito.when(libvirtComputingResource.hostSupportsVlanTrunkXml()).thenReturn(false); + NicTO nic = buildTrunkNic(100, Collections.singletonList(200)); + LibvirtVMDef.InterfaceDef intf = new LibvirtVMDef.InterfaceDef(); + intf.setDevName("vnet5"); + + driver.ensureVlanTrunkMembership(intf, nic); + + Mockito.verify(driver).runBridgeVlanCommand("add", "vnet5", "100", true); + Mockito.verify(driver).runBridgeVlanCommand("add", "vnet5", "200", false); + } + + @Test(expected = InternalErrorException.class) + public void ensureVlanTrunkMembershipFailsWhenTapNameUnknown() throws InternalErrorException { + Mockito.when(libvirtComputingResource.hostSupportsVlanTrunkXml()).thenReturn(false); + NicTO nic = buildTrunkNic(100, null); + LibvirtVMDef.InterfaceDef intf = new LibvirtVMDef.InterfaceDef(); + + driver.ensureVlanTrunkMembership(intf, nic); + } + + @Test + public void updateVlanTrunkMembershipUpdatesXmlOnModernLibvirt() throws Exception { + Mockito.when(libvirtComputingResource.hostSupportsVlanTrunkXml()).thenReturn(true); + Mockito.when(libvirtComputingResource.hostSupportsVlanFiltering()).thenReturn(true); + NicTO nic = buildTrunkNic(100, Collections.singletonList(200)); + LibvirtVMDef.InterfaceDef intf = new LibvirtVMDef.InterfaceDef(); + intf.setDevName("vnet5"); + org.libvirt.Domain vm = Mockito.mock(org.libvirt.Domain.class); + + driver.updateVlanTrunkMembership(vm, intf, nic); + + Mockito.verify(vm).updateDeviceFlags(Mockito.contains("trunk"), + Mockito.eq(org.libvirt.Domain.DeviceModifyFlags.LIVE)); + } + + @Test + public void updateVlanTrunkMembershipAppliesDiffOnOldLibvirt() throws Exception { + Mockito.when(libvirtComputingResource.hostSupportsVlanTrunkXml()).thenReturn(false); + Mockito.when(libvirtComputingResource.hostSupportsVlanFiltering()).thenReturn(true); + NicTO nic = buildTrunkNic(100, Collections.singletonList(200)); + LibvirtVMDef.InterfaceDef intf = new LibvirtVMDef.InterfaceDef(); + intf.setDevName("vnet5"); + org.libvirt.Domain vm = Mockito.mock(org.libvirt.Domain.class); + Map currentMembership = new HashMap<>(); + currentMembership.put(100, true); + currentMembership.put(999, false); + Mockito.doReturn(currentMembership).when(driver).readCurrentVlanMembership("vnet5"); + + driver.updateVlanTrunkMembership(vm, intf, nic); + + Mockito.verify(driver).runBridgeVlanCommand("del", "vnet5", "999", false); + Mockito.verify(driver).runBridgeVlanCommand("add", "vnet5", "200", false); + Mockito.verify(driver, Mockito.never()).runBridgeVlanCommand("add", "vnet5", "100", true); + Mockito.verify(vm, Mockito.never()).updateDeviceFlags(Mockito.anyString(), Mockito.anyInt()); + } + + @Test(expected = InternalErrorException.class) + public void updateVlanTrunkMembershipFailsForNonTrunkNic() throws Exception { + NicTO nic = new NicTO(); + nic.setBroadcastType(Networks.BroadcastDomainType.Vlan); + Mockito.when(libvirtComputingResource.hostSupportsVlanFiltering()).thenReturn(false); + LibvirtVMDef.InterfaceDef intf = new LibvirtVMDef.InterfaceDef(); + org.libvirt.Domain vm = Mockito.mock(org.libvirt.Domain.class); + + driver.updateVlanTrunkMembership(vm, intf, nic); + } + + @Test(expected = InternalErrorException.class) + public void plugTrunkVlanNicFailsForNonVlanPrimaryBroadcastType() throws InternalErrorException { + NicTO nic = new NicTO(); + nic.setBroadcastType(Networks.BroadcastDomainType.Vxlan); + nic.setTrunkVlan(true); + driver.plugTrunkVlanNic(new LibvirtVMDef.InterfaceDef(), nic, null, null, null, 0); + } + + @Test(expected = InternalErrorException.class) + public void plugTrunkVlanNicFailsForNonVlanAssociatedNetwork() throws InternalErrorException { + Mockito.when(libvirtComputingResource.hostSupportsVlanFiltering()).thenReturn(true); + NicTO nic = buildTrunkNic(100, null); + NetworkTO badAssociation = new NetworkTO(); + badAssociation.setBroadcastType(Networks.BroadcastDomainType.Vxlan); + nic.setAssociatedNetworks(Collections.singletonList(badAssociation)); + driver.plugTrunkVlanNic(new LibvirtVMDef.InterfaceDef(), nic, null, null, null, 0); + } + + @Test + public void plugTrunkVlanNicBuildsInterfaceWithAllVlanTagsOnGuestBridge() throws InternalErrorException { + Mockito.when(libvirtComputingResource.hostSupportsVlanTrunkXml()).thenReturn(true); + Mockito.when(libvirtComputingResource.hostSupportsVlanFiltering()).thenReturn(true); + NicTO nic = buildTrunkNic(100, java.util.Arrays.asList(200, 300)); + LibvirtVMDef.InterfaceDef intf = new LibvirtVMDef.InterfaceDef(); + + driver.plugTrunkVlanNic(intf, nic, null, null, null, 0); + + Assert.assertTrue(intf.isVlanTrunk()); + Assert.assertEquals(java.util.Arrays.asList(100, 200, 300), intf.getTrunkVlanTags()); + Assert.assertEquals(Integer.valueOf(100), intf.getNativeVlanTag()); + Assert.assertEquals(BRIDGE_NAME, intf.getBrName()); + } + + @Test + public void plugTrunkVlanNicFailsClearlyWhenAssociatedNetworkHasNoVlanYet() { + Mockito.when(libvirtComputingResource.hostSupportsVlanFiltering()).thenReturn(true); + NicTO nic = buildTrunkNic(100, null); + NetworkTO notYetImplemented = new NetworkTO(); + notYetImplemented.setBroadcastType(Networks.BroadcastDomainType.Vlan); + notYetImplemented.setUuid("unimplemented-network-uuid"); + // broadcastUri intentionally left null, matching a network that has never been implemented + nic.setAssociatedNetworks(Collections.singletonList(notYetImplemented)); + + try { + driver.plugTrunkVlanNic(new LibvirtVMDef.InterfaceDef(), nic, null, null, null, 0); + Assert.fail("expected InternalErrorException"); + } catch (InternalErrorException e) { + Assert.assertTrue(e.getMessage().contains("unimplemented-network-uuid")); + } + } + + @Test + public void plugTrunkVlanNicUsesTrafficLabelBridgeWhenPresent() throws InternalErrorException { + Mockito.when(libvirtComputingResource.hostSupportsVlanTrunkXml()).thenReturn(true); + Mockito.when(libvirtComputingResource.hostSupportsVlanFiltering()).thenReturn(true); + NicTO nic = buildTrunkNic(100, null); + LibvirtVMDef.InterfaceDef intf = new LibvirtVMDef.InterfaceDef(); + + driver.plugTrunkVlanNic(intf, nic, "customLabel", null, null, 0); + + Assert.assertEquals("customLabel", intf.getBrName()); + Assert.assertEquals(Collections.singletonList(100), intf.getTrunkVlanTags()); + Assert.assertEquals(Integer.valueOf(100), intf.getNativeVlanTag()); + } + + @Test + public void plugTrunkVlanNicNeverModifiesTheUplinkItself() throws InternalErrorException { + Mockito.when(libvirtComputingResource.hostSupportsVlanTrunkXml()).thenReturn(true); + Mockito.when(libvirtComputingResource.hostSupportsVlanFiltering()).thenReturn(true); + Mockito.doReturn(Collections.emptyMap()).when(driver).readCurrentVlanMembership(Mockito.anyString()); + + driver.plugTrunkVlanNic(new LibvirtVMDef.InterfaceDef(), buildTrunkNic(100, null), null, null, null, 0); + driver.plugTrunkVlanNic(new LibvirtVMDef.InterfaceDef(), buildTrunkNic(300, null), "customLabel", null, null, 0); + + // the uplink's own VLAN membership is the operator's responsibility, not CloudStack's - plugging a nic + // never adds anything to eth1/eth2, regardless of whether their current membership already covers the VLAN + Mockito.verify(driver, Mockito.never()).runBridgeVlanCommand(Mockito.eq("add"), Mockito.eq("eth1"), Mockito.anyString(), Mockito.anyBoolean()); + Mockito.verify(driver, Mockito.never()).runBridgeVlanCommand(Mockito.eq("add"), Mockito.eq("eth2"), Mockito.anyString(), Mockito.anyBoolean()); + } + + @Test + public void plugTrunkVlanNicChecksUplinkMembershipForTheResolvedPif() throws InternalErrorException { + Mockito.when(libvirtComputingResource.hostSupportsVlanTrunkXml()).thenReturn(true); + Mockito.when(libvirtComputingResource.hostSupportsVlanFiltering()).thenReturn(true); + Mockito.doReturn(Collections.emptyMap()).when(driver).readCurrentVlanMembership(Mockito.anyString()); + + driver.plugTrunkVlanNic(new LibvirtVMDef.InterfaceDef(), buildTrunkNic(100, Collections.singletonList(200)), null, null, null, 0); + + Mockito.verify(driver).readCurrentVlanMembership("eth1"); + } + + @Test + public void plugTrunkVlanNicSucceedsWhenUplinkMembershipCannotBeRead() throws InternalErrorException { + Mockito.when(libvirtComputingResource.hostSupportsVlanTrunkXml()).thenReturn(true); + Mockito.when(libvirtComputingResource.hostSupportsVlanFiltering()).thenReturn(true); + Mockito.doThrow(new InternalErrorException("bridge command failed")).when(driver).readCurrentVlanMembership(Mockito.anyString()); + LibvirtVMDef.InterfaceDef intf = new LibvirtVMDef.InterfaceDef(); + + // a failure to even read the uplink's current membership is only a missed diagnostic, never a reason to refuse the plug + driver.plugTrunkVlanNic(intf, buildTrunkNic(100, null), null, null, null, 0); + + Assert.assertEquals(BRIDGE_NAME, intf.getBrName()); + } + + @Test + public void plugTrunkVlanNicSucceedsWhenUplinkPifUnknownForBridge() throws InternalErrorException { + Mockito.when(libvirtComputingResource.hostSupportsVlanTrunkXml()).thenReturn(true); + Mockito.when(libvirtComputingResource.hostSupportsVlanFiltering()).thenReturn(true); + NicTO nic = buildTrunkNic(100, null); + LibvirtVMDef.InterfaceDef intf = new LibvirtVMDef.InterfaceDef(); + + // not being able to even determine the uplink is only a missed diagnostic, never a reason to refuse the plug + driver.plugTrunkVlanNic(intf, nic, "unknownLabel", null, null, 0); + + Assert.assertEquals("unknownLabel", intf.getBrName()); + } } diff --git a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResourceTest.java b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResourceTest.java index 008d9444fe5f..3cbd9c628f7c 100644 --- a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResourceTest.java +++ b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResourceTest.java @@ -1749,10 +1749,12 @@ public void testMigrateCommand() { final VirtualMachineTO vmTO = Mockito.mock(VirtualMachineTO.class); final boolean executeInSequence = false; DiskTO[] diskTOS = new DiskTO[]{}; + final NicTO[] nicTOs = new NicTO[]{}; final MigrateCommand command = new MigrateCommand(vmName, destIp, isWindows, vmTO, executeInSequence ); when(libvirtComputingResourceMock.getLibvirtUtilitiesHelper()).thenReturn(libvirtUtilitiesHelper); when(vmTO.getDisks()).thenReturn(diskTOS); + when(vmTO.getNics()).thenReturn(nicTOs); try { when(libvirtUtilitiesHelper.getConnectionByVmName(vmName)).thenReturn(conn); when(libvirtUtilitiesHelper.retrieveQemuConnection("qemu+tcp://" + command.getDestinationIp() + "/system")).thenReturn(dconn); diff --git a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtVMDefTest.java b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtVMDefTest.java index 56ad267eac7e..b94ba691155f 100644 --- a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtVMDefTest.java +++ b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtVMDefTest.java @@ -140,6 +140,68 @@ public void testInterfaceBridgeSlot() { assertEquals(expected, ifDef.toString()); } + @Test + public void testInterfaceLegacySingleVlanTag() { + LibvirtVMDef.InterfaceDef ifDef = new LibvirtVMDef.InterfaceDef(); + ifDef.defBridgeNet("targetDeviceName", null, "00:11:22:aa:bb:dd", LibvirtVMDef.InterfaceDef.NicModel.VIRTIO); + ifDef.setVlanTag(123); + + String expected = + "\n" + + "\n" + + "\n" + + "\n" + + "\n\n" + + "\n" + + "\n"; + + assertEquals(expected, ifDef.toString()); + assertFalse(ifDef.isVlanTrunk()); + } + + @Test + public void testInterfaceTrunkVlanTags() { + LibvirtVMDef.InterfaceDef ifDef = new LibvirtVMDef.InterfaceDef(); + ifDef.defBridgeNet("cloudbr1", null, "00:11:22:aa:bb:dd", LibvirtVMDef.InterfaceDef.NicModel.VIRTIO); + ifDef.setTrunkVlanTags(Arrays.asList(100, 200, 300), 100); + + String expected = + "\n" + + "\n" + + "\n" + + "\n" + + "\n\n\n\n" + + "\n" + + "\n"; + + assertEquals(expected, ifDef.toString()); + assertTrue(ifDef.isVlanTrunk()); + assertEquals(Arrays.asList(100, 200, 300), ifDef.getTrunkVlanTags()); + assertEquals(Integer.valueOf(100), ifDef.getNativeVlanTag()); + } + + @Test + public void testInterfaceTrunkVlanTagsWithoutNativeVlan() { + LibvirtVMDef.InterfaceDef ifDef = new LibvirtVMDef.InterfaceDef(); + ifDef.defBridgeNet("cloudbr1", null, "00:11:22:aa:bb:dd", LibvirtVMDef.InterfaceDef.NicModel.VIRTIO); + ifDef.setTrunkVlanTags(Arrays.asList(100, 200, 300), null); + + String content = ifDef.toString(); + assertFalse(content.contains("nativeMode")); + } + + @Test + public void testInterfaceTrunkVlanTagsTakesPrecedenceOverLegacyVlanTag() { + LibvirtVMDef.InterfaceDef ifDef = new LibvirtVMDef.InterfaceDef(); + ifDef.defBridgeNet("cloudbr1", null, "00:11:22:aa:bb:dd", LibvirtVMDef.InterfaceDef.NicModel.VIRTIO); + ifDef.setVlanTag(50); + ifDef.setTrunkVlanTags(Arrays.asList(50, 60), 50); + + String content = ifDef.toString(); + assertTrue(content.contains("")); + assertFalse(content.contains("trunk='no'")); + } + @Test public void testInterfaceWithMultiQueueAndPacked() { LibvirtVMDef.InterfaceDef ifDef = new LibvirtVMDef.InterfaceDef(); diff --git a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtMigrateCommandWrapperTest.java b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtMigrateCommandWrapperTest.java index aa36fb89765c..410f5137cbda 100644 --- a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtMigrateCommandWrapperTest.java +++ b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtMigrateCommandWrapperTest.java @@ -26,6 +26,7 @@ import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Collections; import java.util.Arrays; import java.util.HashMap; import java.util.List; @@ -66,7 +67,10 @@ import com.cloud.agent.api.VgpuTypesInfo; import com.cloud.agent.api.to.DpdkTO; import com.cloud.agent.api.to.GPUDeviceTO; +import com.cloud.agent.api.to.NetworkTO; +import com.cloud.agent.api.to.NicTO; import com.cloud.agent.api.to.VirtualMachineTO; +import com.cloud.network.Networks; import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; import com.cloud.hypervisor.kvm.resource.LibvirtConnection; import com.cloud.hypervisor.kvm.resource.LibvirtVMDef.DiskDef; @@ -956,6 +960,118 @@ public void testReplaceDPDKPorts() throws ParserConfigurationException, IOExcept Assert.assertFalse(replaced.contains("csdpdk-1")); } + private static final String vlanNicMac = "02:00:00:00:00:01"; + private static final String vlanNicDomainXml = + "\n" + + " i-2-4-VM\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + ""; + + private NicTO buildVlanNicTO(boolean trunk, int primaryVlan, List associatedVlans) { + NicTO nic = new NicTO(); + nic.setBroadcastType(Networks.BroadcastDomainType.Vlan); + nic.setBroadcastUri(Networks.BroadcastDomainType.Vlan.toUri(primaryVlan)); + nic.setMac(vlanNicMac); + nic.setTrunkVlan(trunk); + if (associatedVlans != null) { + List associated = new ArrayList<>(); + for (Integer vlan : associatedVlans) { + NetworkTO associatedTo = new NetworkTO(); + associatedTo.setBroadcastType(Networks.BroadcastDomainType.Vlan); + associatedTo.setBroadcastUri(Networks.BroadcastDomainType.Vlan.toUri(vlan)); + associated.add(associatedTo); + } + nic.setAssociatedNetworks(associated); + } + return nic; + } + + @Test + public void replaceVlanTrunkInterfacesLeavesOrdinaryVlanNicUntouchedOnNonFilteringSource() + throws ParserConfigurationException, IOException, SAXException, TransformerException { + NicTO nic = buildVlanNicTO(false, 100, null); + Mockito.doReturn(new NicTO[]{nic}).when(virtualMachineTOMock).getNics(); + Mockito.doReturn(virtualMachineTOMock).when(migrateCommandMock).getVirtualMachine(); + Mockito.doReturn(false).when(libvirtComputingResourceMock).hostSupportsVlanFiltering(); + + String result = libvirtMigrateCmdWrapper.replaceVlanTrunkInterfaces(vlanNicDomainXml, migrateCommandMock, libvirtComputingResourceMock); + + assertEquals(vlanNicDomainXml, result); + } + + @Test + public void replaceVlanTrunkInterfacesRewritesOrdinaryNicBridgeWhenOnlyDestIsFilteringEnabled() + throws ParserConfigurationException, IOException, SAXException, TransformerException { + // legacy (non-filtering) source -> shared-bridge destination: the source's own per-VLAN bridge name + // doesn't exist on the destination, so the rewrite must fire even though the source isn't shared-bridge + NicTO nic = buildVlanNicTO(false, 100, null); + Mockito.doReturn(new NicTO[]{nic}).when(virtualMachineTOMock).getNics(); + Mockito.doReturn(virtualMachineTOMock).when(migrateCommandMock).getVirtualMachine(); + Mockito.doReturn(false).when(libvirtComputingResourceMock).hostSupportsVlanFiltering(); + Mockito.doReturn(true).when(migrateCommandMock).getDestVlanFilteringEnabled(); + Mockito.doReturn(false).when(migrateCommandMock).getDestVlanTrunkXmlSupported(); + Mockito.doReturn(Map.of(vlanNicMac, "destcloudbr0")).when(migrateCommandMock).getNicBridgeMapping(); + + String result = libvirtMigrateCmdWrapper.replaceVlanTrunkInterfaces(vlanNicDomainXml, migrateCommandMock, libvirtComputingResourceMock); + + assertTrue(result.contains("bridge=\"destcloudbr0\"")); + } + + @Test + public void replaceVlanTrunkInterfacesInsertsTrunkVlanBlockWhenDestSupportsTrunkXml() + throws ParserConfigurationException, IOException, SAXException, TransformerException { + NicTO nic = buildVlanNicTO(true, 100, Collections.singletonList(200)); + Mockito.doReturn(new NicTO[]{nic}).when(virtualMachineTOMock).getNics(); + Mockito.doReturn(virtualMachineTOMock).when(migrateCommandMock).getVirtualMachine(); + Mockito.doReturn(true).when(libvirtComputingResourceMock).hostSupportsVlanFiltering(); + Mockito.doReturn(true).when(migrateCommandMock).getDestVlanFilteringEnabled(); + Mockito.doReturn(true).when(migrateCommandMock).getDestVlanTrunkXmlSupported(); + Mockito.doReturn(Map.of(vlanNicMac, "destcloudbr0")).when(migrateCommandMock).getNicBridgeMapping(); + + String result = libvirtMigrateCmdWrapper.replaceVlanTrunkInterfaces(vlanNicDomainXml, migrateCommandMock, libvirtComputingResourceMock); + + assertTrue(result.contains("bridge=\"destcloudbr0\"")); + assertTrue(result.contains("trunk=\"yes\"")); + assertTrue(result.contains("id=\"100\"")); + assertTrue(result.contains("nativeMode=\"untagged\"")); + assertTrue(result.contains("id=\"200\"")); + } + + @Test + public void replaceVlanTrunkInterfacesStripsVlanBlockWhenDestDoesNotSupportTrunkXml() + throws ParserConfigurationException, IOException, SAXException, TransformerException { + NicTO nic = buildVlanNicTO(true, 100, Collections.singletonList(200)); + Mockito.doReturn(new NicTO[]{nic}).when(virtualMachineTOMock).getNics(); + Mockito.doReturn(virtualMachineTOMock).when(migrateCommandMock).getVirtualMachine(); + Mockito.doReturn(true).when(libvirtComputingResourceMock).hostSupportsVlanFiltering(); + Mockito.doReturn(true).when(migrateCommandMock).getDestVlanFilteringEnabled(); + Mockito.doReturn(false).when(migrateCommandMock).getDestVlanTrunkXmlSupported(); + Mockito.doReturn(Map.of(vlanNicMac, "destcloudbr0")).when(migrateCommandMock).getNicBridgeMapping(); + + String result = libvirtMigrateCmdWrapper.replaceVlanTrunkInterfaces(vlanNicDomainXml, migrateCommandMock, libvirtComputingResourceMock); + + assertTrue(result.contains("bridge=\"destcloudbr0\"")); + assertFalse(result.contains(" disks = createNonClvmDisks(); + try (MockedStatic mockedConnection = Mockito.mockStatic(LibvirtConnection.class)) { + mockedConnection.when(() -> LibvirtConnection.getConnectionByVmName(VM_NAME)).thenReturn(connect); + when(libvirtComputingResource.getDisks(connect, VM_NAME)).thenReturn(disks); + + Answer answer = wrapper.execute(postMigrationCommand, libvirtComputingResource); + + Assert.assertTrue(answer.getResult()); + Mockito.verify(vifDriver).ensureVlanTrunkMembership(liveInterface, vlanNic); + } + } + + @Test + public void testExecute_NonVlanNic_SkipsManualVlanTrunkMembership() throws LibvirtException { + NicTO publicNic = new NicTO(); + publicNic.setBroadcastType(Networks.BroadcastDomainType.Native); + publicNic.setMac("02:00:00:00:00:02"); + when(virtualMachineTO.getNics()).thenReturn(new NicTO[]{publicNic}); + + List disks = createNonClvmDisks(); + try (MockedStatic mockedConnection = Mockito.mockStatic(LibvirtConnection.class)) { + mockedConnection.when(() -> LibvirtConnection.getConnectionByVmName(VM_NAME)).thenReturn(connect); + when(libvirtComputingResource.getDisks(connect, VM_NAME)).thenReturn(disks); + + Answer answer = wrapper.execute(postMigrationCommand, libvirtComputingResource); + + Assert.assertTrue(answer.getResult()); + Mockito.verify(libvirtComputingResource, Mockito.never()).getInterface(any(), any(), any()); + } + } + @Test public void testExecute_ClvmVolumes_ConvertedToExclusiveMode() throws LibvirtException { List disks = createClvmDisks(); diff --git a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtPrepareForMigrationCommandWrapperTest.java b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtPrepareForMigrationCommandWrapperTest.java index 864cb4482886..9a98b5c99fb2 100644 --- a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtPrepareForMigrationCommandWrapperTest.java +++ b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtPrepareForMigrationCommandWrapperTest.java @@ -57,17 +57,28 @@ public void createPrepareForMigrationAnswerTestDpdkInterfaceNotEmptyShouldSetPar Map dpdkInterfaceMapping = new HashMap<>(); dpdkInterfaceMapping.put("Interface", new DpdkTO()); - PrepareForMigrationAnswer prepareForMigrationAnswer = libvirtPrepareForMigrationCommandWrapperSpy.createPrepareForMigrationAnswer(prepareForMigrationCommandMock, dpdkInterfaceMapping, libvirtComputingResourceMock, - virtualMachineTOMock); + PrepareForMigrationAnswer prepareForMigrationAnswer = libvirtPrepareForMigrationCommandWrapperSpy.createPrepareForMigrationAnswer(prepareForMigrationCommandMock, dpdkInterfaceMapping, null, + libvirtComputingResourceMock, virtualMachineTOMock); Assert.assertEquals(prepareForMigrationAnswer.getDpdkInterfaceMapping(), dpdkInterfaceMapping); } + @Test + public void createPrepareForMigrationAnswerTestNicBridgeMappingNotEmptyShouldSetParamOnAnswer() { + Map nicBridgeMapping = new HashMap<>(); + nicBridgeMapping.put("02:00:00:00:00:01", "cloudbr0"); + + PrepareForMigrationAnswer prepareForMigrationAnswer = libvirtPrepareForMigrationCommandWrapperSpy.createPrepareForMigrationAnswer(prepareForMigrationCommandMock, null, nicBridgeMapping, + libvirtComputingResourceMock, virtualMachineTOMock); + + Assert.assertEquals(nicBridgeMapping, prepareForMigrationAnswer.getNicBridgeMapping()); + } + @Test public void createPrepareForMigrationAnswerTestVerifyThatCpuSharesIsSet() { int cpuShares = 1000; Mockito.doReturn(cpuShares).when(libvirtComputingResourceMock).calculateCpuShares(virtualMachineTOMock); - PrepareForMigrationAnswer prepareForMigrationAnswer = libvirtPrepareForMigrationCommandWrapperSpy.createPrepareForMigrationAnswer(prepareForMigrationCommandMock,null, + PrepareForMigrationAnswer prepareForMigrationAnswer = libvirtPrepareForMigrationCommandWrapperSpy.createPrepareForMigrationAnswer(prepareForMigrationCommandMock, null, null, libvirtComputingResourceMock, virtualMachineTOMock); Assert.assertEquals(cpuShares, prepareForMigrationAnswer.getNewVmCpuShares().intValue()); diff --git a/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/motion/LinstorDataMotionStrategy.java b/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/motion/LinstorDataMotionStrategy.java index 0dd0de70464f..113805762e2c 100644 --- a/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/motion/LinstorDataMotionStrategy.java +++ b/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/motion/LinstorDataMotionStrategy.java @@ -37,12 +37,15 @@ import com.cloud.agent.api.Answer; import com.cloud.agent.api.MigrateAnswer; import com.cloud.agent.api.MigrateCommand; +import com.cloud.agent.api.PrepareForMigrationAnswer; import com.cloud.agent.api.PrepareForMigrationCommand; +import com.cloud.agent.api.VlanTrunkMigrationHelper; import com.cloud.agent.api.to.DataObjectType; import com.cloud.agent.api.to.VirtualMachineTO; import com.cloud.exception.AgentUnavailableException; import com.cloud.exception.OperationTimedoutException; import com.cloud.host.Host; +import com.cloud.host.dao.HostDetailsDao; import com.cloud.hypervisor.Hypervisor; import com.cloud.storage.Storage; import com.cloud.storage.StorageManager; @@ -54,6 +57,7 @@ import com.cloud.storage.dao.VolumeDao; import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.vm.VMInstanceVO; +import com.cloud.vm.dao.NicDao; import com.cloud.vm.dao.VMInstanceDao; import org.apache.cloudstack.engine.subsystem.api.storage.CopyCommandResult; import org.apache.cloudstack.engine.subsystem.api.storage.DataMotionStrategy; @@ -116,6 +120,10 @@ public class LinstorDataMotionStrategy implements DataMotionStrategy { @Inject private AgentManager _agentManager; @Inject + private HostDetailsDao hostDetailsDao; + @Inject + private NicDao nicDao; + @Inject private PrimaryDataStoreDao _storagePoolDao; @Override @@ -441,8 +449,9 @@ public void copyAsync(Map volumeDataStoreMap, VirtualMach } PrepareForMigrationCommand pfmc = new PrepareForMigrationCommand(vmTO); + Answer pfma; try { - Answer pfma = _agentManager.send(destHost.getId(), pfmc); + pfma = _agentManager.send(destHost.getId(), pfmc); if (pfma == null || !pfma.getResult()) { String details = pfma != null ? pfma.getDetails() : "null answer returned"; @@ -471,11 +480,17 @@ public void copyAsync(Map volumeDataStoreMap, VirtualMach boolean kvmAutoConvergence = StorageManager.KvmAutoConvergence.value(); migrateCommand.setAutoConvergence(kvmAutoConvergence); + VlanTrunkMigrationHelper.populateVlanTrunkMigrationDetails(migrateCommand, (PrepareForMigrationAnswer) pfma, hostDetailsDao, destHost.getId()); + MigrateAnswer migrateAnswer = (MigrateAnswer) _agentManager.send(srcHost.getId(), migrateCommand); boolean success = migrateAnswer != null && migrateAnswer.getResult(); handlePostMigration(success, srcVolumeInfoToDestVolumeInfo, vmTO, destHost); + if (success) { + VlanTrunkMigrationHelper.sendPostMigrationVlanTrunkMembershipIfNeeded(vmTO, destHost.getId(), nicDao, hostDetailsDao, _agentManager::send, logger); + } + if (migrateAnswer == null) { throw new CloudRuntimeException("Unable to get an answer to the migrate command"); } diff --git a/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/motion/StorPoolDataMotionStrategy.java b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/motion/StorPoolDataMotionStrategy.java index 8d3dc868276c..16b176844faf 100644 --- a/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/motion/StorPoolDataMotionStrategy.java +++ b/plugins/storage/volume/storpool/src/main/java/org/apache/cloudstack/storage/motion/StorPoolDataMotionStrategy.java @@ -27,7 +27,9 @@ import com.cloud.agent.api.MigrateCommand.MigrateDiskInfo; import com.cloud.agent.api.ModifyTargetsAnswer; import com.cloud.agent.api.ModifyTargetsCommand; +import com.cloud.agent.api.PrepareForMigrationAnswer; import com.cloud.agent.api.PrepareForMigrationCommand; +import com.cloud.agent.api.VlanTrunkMigrationHelper; import com.cloud.agent.api.storage.StorPoolBackupTemplateFromSnapshotCommand; import com.cloud.agent.api.to.DataObjectType; import com.cloud.agent.api.to.VirtualMachineTO; @@ -37,6 +39,7 @@ import com.cloud.host.Host; import com.cloud.host.HostVO; import com.cloud.host.dao.HostDao; +import com.cloud.host.dao.HostDetailsDao; import com.cloud.hypervisor.Hypervisor.HypervisorType; import com.cloud.storage.DataStoreRole; import com.cloud.storage.Storage.ImageFormat; @@ -54,6 +57,7 @@ import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.vm.VMInstanceVO; import com.cloud.vm.VirtualMachineManager; +import com.cloud.vm.dao.NicDao; import com.cloud.vm.dao.VMInstanceDao; import org.apache.cloudstack.engine.subsystem.api.storage.CopyCommandResult; @@ -120,6 +124,10 @@ public class StorPoolDataMotionStrategy implements DataMotionStrategy { @Inject private HostDao _hostDao; @Inject + private HostDetailsDao hostDetailsDao; + @Inject + private NicDao nicDao; + @Inject private SnapshotDetailsDao _snapshotDetailsDao; @Inject private VMTemplateDetailsDao _vmTemplateDetailsDao; @@ -342,8 +350,9 @@ public void copyAsync(Map volumeDataStoreMap, VirtualMach PrepareForMigrationCommand pfmc = new PrepareForMigrationCommand(vmTO); + Answer pfma; try { - Answer pfma = _agentManager.send(destHost.getId(), pfmc); + pfma = _agentManager.send(destHost.getId(), pfmc); if (pfma == null || !pfma.getResult()) { String details = pfma != null ? pfma.getDetails() : "null answer returned"; @@ -371,12 +380,18 @@ public void copyAsync(Map volumeDataStoreMap, VirtualMach migrateCommand.setAutoConvergence(kvmAutoConvergence); + VlanTrunkMigrationHelper.populateVlanTrunkMigrationDetails(migrateCommand, (PrepareForMigrationAnswer) pfma, hostDetailsDao, destHost.getId()); + MigrateAnswer migrateAnswer = (MigrateAnswer) _agentManager.send(srcHost.getId(), migrateCommand); boolean success = migrateAnswer != null && migrateAnswer.getResult(); handlePostMigration(success, srcVolumeInfoToDestVolumeInfo, vmTO, destHost); + if (success) { + VlanTrunkMigrationHelper.sendPostMigrationVlanTrunkMembershipIfNeeded(vmTO, destHost.getId(), nicDao, hostDetailsDao, _agentManager::send, logger); + } + if (migrateAnswer == null) { throw new CloudRuntimeException("Unable to get an answer to the migrate command"); } diff --git a/server/src/main/java/com/cloud/api/ApiDBUtils.java b/server/src/main/java/com/cloud/api/ApiDBUtils.java index 934600eb2b61..bd7e6b8c483c 100644 --- a/server/src/main/java/com/cloud/api/ApiDBUtils.java +++ b/server/src/main/java/com/cloud/api/ApiDBUtils.java @@ -48,6 +48,7 @@ import org.apache.cloudstack.api.response.BackupOfferingResponse; import org.apache.cloudstack.api.response.DiskOfferingResponse; import org.apache.cloudstack.api.response.DomainResponse; +import org.apache.cloudstack.api.response.NicNetworkMapResponse; import org.apache.cloudstack.api.response.DomainRouterResponse; import org.apache.cloudstack.api.response.EventResponse; import org.apache.cloudstack.api.response.SharedFSResponse; @@ -339,6 +340,8 @@ import com.cloud.uservm.UserVm; import com.cloud.utils.EnumUtils; import com.cloud.utils.Pair; +import com.cloud.utils.StringUtils; +import com.cloud.utils.net.NetUtils; import com.cloud.vm.ConsoleProxyVO; import com.cloud.vm.DomainRouterVO; import com.cloud.vm.InstanceGroup; @@ -356,6 +359,8 @@ import com.cloud.vm.dao.ConsoleProxyDao; import com.cloud.vm.dao.DomainRouterDao; import com.cloud.vm.dao.NicDao; +import com.cloud.vm.dao.NicNetworkMapDao; +import com.cloud.vm.dao.NicNetworkMapVO; import com.cloud.vm.dao.NicSecondaryIpDao; import com.cloud.vm.dao.NicSecondaryIpVO; import com.cloud.vm.dao.UserVmDao; @@ -487,6 +492,7 @@ public class ApiDBUtils { static VMSnapshotDao s_vmSnapshotDao; static ClusterDetailsDao s_clusterDetailsDao; static NicSecondaryIpDao s_nicSecondaryIpDao; + static NicNetworkMapDao s_nicNetworkMapDao; static VpcProvisioningService s_vpcProvSvc; static AffinityGroupDao s_affinityGroupDao; static AffinityGroupJoinDao s_affinityGroupJoinDao; @@ -736,6 +742,8 @@ public class ApiDBUtils { @Inject private NicSecondaryIpDao nicSecondaryIpDao; @Inject + private NicNetworkMapDao nicNetworkMapDao; + @Inject private VpcProvisioningService vpcProvSvc; @Inject private AffinityGroupDao affinityGroupDao; @@ -903,6 +911,7 @@ void init() { s_vmSnapshotDao = vmSnapshotDao; s_nicDao = nicDao; s_nicSecondaryIpDao = nicSecondaryIpDao; + s_nicNetworkMapDao = nicNetworkMapDao; s_vpcProvSvc = vpcProvSvc; s_affinityGroupDao = affinityGroupDao; s_affinityGroupJoinDao = affinityGroupJoinDao; @@ -2246,6 +2255,46 @@ public static List findNicSecondaryIps(long nicId) { return s_nicSecondaryIpDao.listByNicId(nicId); } + /** + * Builds the response for every additional network a multi-VLAN trunk nic is associated with (beyond its + * primary network). Shared by every response builder that surfaces a nic's associations - {@code listNics}, + * {@code listVirtualMachines}, and the associate/disassociate/change-primary command responses all need the + * identical shape, so it's built once here rather than duplicated per builder. + */ + public static List findAssociatedNetworkResponses(long nicId) { + List associations = s_nicNetworkMapDao.listByNicId(nicId); + List associatedNetworkResponses = new ArrayList<>(); + for (NicNetworkMapVO association : associations) { + NetworkVO associatedNetwork = findNetworkById(association.getNetworkId()); + if (associatedNetwork == null) { + continue; + } + NicNetworkMapResponse associationResponse = new NicNetworkMapResponse(); + associationResponse.setNetworkId(associatedNetwork.getUuid()); + associationResponse.setNetworkName(associatedNetwork.getName()); + if (associatedNetwork.getBroadcastUri() != null) { + associationResponse.setBroadcastUri(associatedNetwork.getBroadcastUri().toString()); + } + // gateway/netmask/IPv6 gateway/IPv6 CIDR describe the associated network itself, not this nic's + // specific address on it - sourced the same way ApiResponseHelper#createNetworkResponse does for + // NetworkResponse. A Shared network with more than one IP range has these as a comma-separated + // list (one per range); only the first is surfaced here, matching that same existing convention. + associationResponse.setGateway(StringUtils.getFirstValueFromCommaSeparatedString(associatedNetwork.getGateway())); + String cidr = StringUtils.getFirstValueFromCommaSeparatedString(associatedNetwork.getCidr()); + if (associatedNetwork.getNetworkCidr() != null) { + associationResponse.setNetmask(NetUtils.cidr2Netmask(associatedNetwork.getNetworkCidr())); + } else if (cidr != null) { + associationResponse.setNetmask(NetUtils.cidr2Netmask(cidr)); + } + associationResponse.setIp6Gateway(StringUtils.getFirstValueFromCommaSeparatedString(associatedNetwork.getIp6Gateway())); + associationResponse.setIp6Cidr(StringUtils.getFirstValueFromCommaSeparatedString(associatedNetwork.getIp6Cidr())); + associationResponse.setIpAddress(association.getIp4Address()); + associationResponse.setIp6Address(association.getIp6Address()); + associatedNetworkResponses.add(associationResponse); + } + return associatedNetworkResponses; + } + public static NicVO findNicById(long nicId) { return s_nicDao.findById(nicId); } diff --git a/server/src/main/java/com/cloud/api/ApiResponseHelper.java b/server/src/main/java/com/cloud/api/ApiResponseHelper.java index f56cda6e557a..150d04ddc0d8 100644 --- a/server/src/main/java/com/cloud/api/ApiResponseHelper.java +++ b/server/src/main/java/com/cloud/api/ApiResponseHelper.java @@ -4880,6 +4880,11 @@ public NicResponse createNicResponse(Nic result) { response.setSecondaryIps(ipList); } } + /*trunked / associatednetworks - multi-VLAN trunk nic, if any*/ + response.setTrunked(result.getMultiNetwork()); + if (result.getMultiNetwork()) { + response.setAssociatedNetworks(ApiDBUtils.findAssociatedNetworkResponses(result.getId())); + } /*18: extra dhcp options */ List nicExtraDhcpOptionResponses = nicExtraDhcpOptionVOs .stream() diff --git a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java index d700fba5a787..5f698cc1767a 100644 --- a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java +++ b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java @@ -376,6 +376,7 @@ import com.cloud.vm.dao.DomainRouterDao; import com.cloud.vm.dao.InstanceGroupVMMapDao; import com.cloud.vm.dao.NicDao; +import com.cloud.vm.dao.NicNetworkMapDao; import com.cloud.vm.dao.UserVmDao; import com.cloud.vm.dao.VMInstanceDao; import com.cloud.vm.dao.VMInstanceDetailsDao; @@ -592,6 +593,9 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q @Inject NicDao nicDao; + @Inject + NicNetworkMapDao nicNetworkMapDao; + @Inject HostDao hostDao; @@ -1776,9 +1780,45 @@ private Pair, Integer> searchForUserVMIdsAndCount(ListVMsCmd cmd) { Integer count = uniqueVmPair.second(); List vmIds = uniqueVmPair.first().stream().map(VMInstanceVO::getId).collect(Collectors.toList()); + + if (networkId != null) { + return addVmsWithNetworkAssociation(vmIds, count, networkId, domainId, isRecursive, permittedAccounts, listProjectResourcesCriteria); + } return new Pair<>(vmIds, count); } + // A nic whose primary network is something else but is also associated with networkId (multi-VLAN trunk nic) + // is invisible to the primary-nic-only search above, so it needs a separate, additive lookup here. The ACL + // check is re-applied via the same builder the primary search above already used, rather than re-implemented, + // so visibility rules can't drift between the two. + private Pair, Integer> addVmsWithNetworkAssociation(List vmIds, Integer count, Long networkId, Long domainId, Boolean isRecursive, + List permittedAccounts, ListProjectResourcesCriteria listProjectResourcesCriteria) { + Set candidateVmIds = new HashSet<>(); + for (Long nicId : nicNetworkMapDao.listNicIdsByNetworkId(networkId)) { + NicVO nic = nicDao.findById(nicId); + if (nic != null && !vmIds.contains(nic.getInstanceId())) { + candidateVmIds.add(nic.getInstanceId()); + } + } + if (candidateVmIds.isEmpty()) { + return new Pair<>(vmIds, count); + } + + SearchBuilder assocSearchBuilder = userVmDao.createSearchBuilder(); + accountMgr.buildACLSearchBuilder(assocSearchBuilder, domainId, isRecursive, permittedAccounts, listProjectResourcesCriteria); + assocSearchBuilder.and("idIN", assocSearchBuilder.entity().getId(), Op.IN); + SearchCriteria assocCriteria = assocSearchBuilder.create(); + accountMgr.buildACLSearchCriteria(assocCriteria, domainId, isRecursive, permittedAccounts, listProjectResourcesCriteria); + assocCriteria.setParameters("idIN", candidateVmIds.toArray()); + List visibleAssociatedVms = userVmDao.search(assocCriteria, null); + + List mergedIds = new ArrayList<>(vmIds); + for (UserVmVO vm : visibleAssociatedVms) { + mergedIds.add(vm.getId()); + } + return new Pair<>(mergedIds, count + visibleAssociatedVms.size()); + } + @Override public ListResponse searchForSecurityGroups(ListSecurityGroupsCmd cmd) { Pair, Integer> result = searchForSecurityGroupsInternal(cmd); 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 2e4025ca0013..f942a38746c6 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 @@ private void setNewHostResponseBase(HostJoinVO host, EnumSet detail } 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, diff --git a/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java b/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java index ebc2326a72d5..1836ed41b090 100644 --- a/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java +++ b/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java @@ -104,6 +104,8 @@ import com.cloud.vm.VmIsoMapVO; import com.cloud.vm.VmStats; import com.cloud.vm.dao.NicExtraDhcpOptionDao; +import com.cloud.vm.dao.NicNetworkMapDao; +import com.cloud.vm.dao.NicNetworkMapVO; import com.cloud.vm.dao.NicSecondaryIpVO; import com.cloud.vm.dao.VMInstanceDetailsDao; import com.cloud.vm.dao.VmIsoMapDao; @@ -122,6 +124,8 @@ public class UserVmJoinDaoImpl extends GenericDaoBaseWithTagInformation nicExtraDhcpOptionResponses = _nicExtraDhcpOptionDao.listByNicId(nic_id).stream() @@ -630,6 +636,20 @@ private void addVnfInfoToserVmResponse(UserVmJoinVO userVm, UserVmResponse userV } } + /** + * The join view backing UserVmJoinVO doesn't select nics.multi_network, so trunk status is derived directly + * from whether nic_network_map has any rows for this nic, rather than adding a column to that wide, + * heavily-used view. + */ + private void populateTrunkNicResponse(NicResponse nicResponse, long nicId) { + List associations = _nicNetworkMapDao.listByNicId(nicId); + nicResponse.setTrunked(!associations.isEmpty()); + if (associations.isEmpty()) { + return; + } + nicResponse.setAssociatedNetworks(ApiDBUtils.findAssociatedNetworkResponses(nicId)); + } + private void addVmRxTxDataToResponse(UserVmJoinVO userVm, UserVmResponse userVmResponse) { Long bytesReceived = 0L; Long bytesSent = 0L; @@ -737,6 +757,8 @@ public UserVmResponse setUserVmResponse(ResponseView view, UserVmResponse userVm nicResponse.setPublicIp(publicIp.getAddress().toString()); } + populateTrunkNicResponse(nicResponse, nic_id); + /* 18: extra dhcp options */ nicResponse.setObjectName("nic"); List nicExtraDhcpOptionResponses = _nicExtraDhcpOptionDao.listByNicId(nic_id) diff --git a/server/src/main/java/com/cloud/deploy/DeploymentPlanningManagerImpl.java b/server/src/main/java/com/cloud/deploy/DeploymentPlanningManagerImpl.java index 4c062b1eaff1..e158429c42c2 100644 --- a/server/src/main/java/com/cloud/deploy/DeploymentPlanningManagerImpl.java +++ b/server/src/main/java/com/cloud/deploy/DeploymentPlanningManagerImpl.java @@ -151,6 +151,8 @@ import com.cloud.vm.VirtualMachine.Event; import com.cloud.vm.VirtualMachine.State; import com.cloud.vm.VirtualMachineProfile; +import com.cloud.vm.NicVO; +import com.cloud.vm.dao.NicDao; import com.cloud.vm.dao.UserVmDao; import com.cloud.vm.dao.VMInstanceDao; @@ -188,6 +190,8 @@ public class DeploymentPlanningManagerImpl extends ManagerBase implements Deploy HostDetailsDao _hostDetailsDao; @Inject private VMTemplateDao templateDao; + @Inject + protected NicDao _nicDao; private static final long ADMIN_ACCOUNT_ROLE_ID = 1l; private static final long INITIAL_RESERVATION_RELEASE_CHECKER_DELAY = 30L * 1000L; // thirty seconds expressed in milliseconds @@ -336,6 +340,7 @@ public DeployDestination planDeployment(VirtualMachineProfile vmProfile, Deploym avoidDisabledResources(vmProfile, dc, avoids); avoidDifferentArchResources(vmProfile, dc, avoids); + avoidHostsNotReadyForMultiNetworkNics(vmProfile, dc, avoids); String haVmTag = (String)vmProfile.getParameter(VirtualMachineProfile.Param.HaTag); String uefiFlag = (String)vmProfile.getParameter(VirtualMachineProfile.Param.UefiFlag); @@ -470,6 +475,34 @@ private void avoidDifferentArchResources(VirtualMachineProfile vmProfile, DataCe } } + /** + * A multi-VLAN trunk nic can only be plugged on a host with VLAN filtering enabled on its guest bridge - this is + * a hard capability requirement, not a preference. Excluding every not-ready host up front, in one pass, avoids + * burning retries in the caller's exclude-and-retry loop (VirtualMachineManagerImpl#orchestrateStart) on hosts + * that were never going to work - which matters once a zone has more not-ready hosts than the start.retry budget. + * No-op, and the candidate set is untouched, for a VM with no multi-VLAN trunk nic - the common case. + */ + @Override + public void avoidHostsNotReadyForMultiNetworkNics(VirtualMachineProfile vmProfile, DataCenter dc, ExcludeList avoids) { + boolean hasMultiNetworkNic = _nicDao.listByVmId(vmProfile.getId()).stream().anyMatch(NicVO::getMultiNetwork); + if (!hasMultiNetworkNic) { + return; + } + List candidateHostIds = _hostDao.listEnabledIdsByDataCenterId(dc.getId()); + if (CollectionUtils.isEmpty(candidateHostIds)) { + return; + } + Set readyHostIds = _hostDetailsDao.findHostIdsWithVlanFilteringEnabled(); + List notReadyHostIds = candidateHostIds.stream() + .filter(hostId -> !readyHostIds.contains(hostId)) + .collect(Collectors.toList()); + if (!notReadyHostIds.isEmpty()) { + logger.debug("Excluding {} host(s) without VLAN filtering enabled, since VM [{}] has a multi-VLAN trunk nic", + notReadyHostIds.size(), vmProfile); + avoids.addHostList(notReadyHostIds); + } + } + private DeployDestination deployInVmLastHost(VirtualMachineProfile vmProfile, DeploymentPlan plan, ExcludeList avoids, DeploymentPlanner planner, VirtualMachine vm, DataCenter dc, ServiceOffering offering, int cpuRequested, long ramRequested, boolean volumesRequireEncryption) throws InsufficientServerCapacityException { diff --git a/server/src/main/java/com/cloud/hypervisor/HypervisorGuruBase.java b/server/src/main/java/com/cloud/hypervisor/HypervisorGuruBase.java index 943d1e73c4b5..adc3b0e4ad36 100644 --- a/server/src/main/java/com/cloud/hypervisor/HypervisorGuruBase.java +++ b/server/src/main/java/com/cloud/hypervisor/HypervisorGuruBase.java @@ -67,6 +67,7 @@ import com.cloud.agent.api.Command; import com.cloud.agent.api.to.DataStoreTO; import com.cloud.agent.api.to.DiskTO; +import com.cloud.agent.api.to.NetworkTO; import com.cloud.agent.api.to.NicTO; import com.cloud.agent.api.to.VirtualMachineTO; import com.cloud.configuration.ConfigurationManager; @@ -97,6 +98,8 @@ import com.cloud.vm.VirtualMachine; import com.cloud.vm.VirtualMachineProfile; import com.cloud.vm.dao.NicDao; +import com.cloud.vm.dao.NicNetworkMapDao; +import com.cloud.vm.dao.NicNetworkMapVO; import com.cloud.vm.dao.NicSecondaryIpDao; import com.cloud.vm.dao.VMInstanceDetailsDao; import com.cloud.vm.dao.VMInstanceDao; @@ -127,6 +130,8 @@ public abstract class HypervisorGuruBase extends AdapterBase implements Hypervis @Inject private NicSecondaryIpDao _nicSecIpDao; @Inject + private NicNetworkMapDao nicNetworkMapDao; + @Inject private ResourceManager _resourceMgr; @Inject protected ServiceOfferingDetailsDao _serviceOfferingDetailsDao; @@ -241,6 +246,7 @@ public NicTO toNicTO(NicProfile profile) { secIps = _nicSecIpDao.getSecondaryIpAddressesForNic(nicVO.getId()); } to.setNicSecIps(secIps); + setTrunkAssociationsOnNicTO(to, nicVO); } else { logger.warn("Unable to load NicVO for NicProfile {}", profile); //Workaround for dynamically created nics @@ -255,6 +261,33 @@ public NicTO toNicTO(NicProfile profile) { return to; } + private void setTrunkAssociationsOnNicTO(final NicTO to, final NicVO nic) { + if (!nic.getMultiNetwork()) { + return; + } + final List associations = nicNetworkMapDao.listByNicId(nic.getId()); + if (associations.isEmpty()) { + return; + } + final List associatedNetworks = new ArrayList<>(); + for (final NicNetworkMapVO association : associations) { + final NetworkVO associatedNetwork = networkDao.findById(association.getNetworkId()); + if (associatedNetwork == null) { + continue; + } + final NetworkTO associatedTo = new NetworkTO(); + associatedTo.setUuid(associatedNetwork.getUuid()); + associatedTo.setBroadcastType(associatedNetwork.getBroadcastDomainType()); + associatedTo.setBroadcastUri(associatedNetwork.getBroadcastUri()); + associatedTo.setType(associatedNetwork.getTrafficType()); + associatedNetworks.add(associatedTo); + } + if (!associatedNetworks.isEmpty()) { + to.setTrunkVlan(true); + to.setAssociatedNetworks(associatedNetworks); + } + } + private String getNetworkName(long zoneId, long domainId, long accountId, VpcVO vpc, long networkId) { String prefix = String.format("D%s-A%s-Z%s", domainId, accountId, zoneId); if (Objects.isNull(vpc)) { diff --git a/server/src/main/java/com/cloud/network/NetworkModelImpl.java b/server/src/main/java/com/cloud/network/NetworkModelImpl.java index f47046cdc434..ea5d394cae1b 100644 --- a/server/src/main/java/com/cloud/network/NetworkModelImpl.java +++ b/server/src/main/java/com/cloud/network/NetworkModelImpl.java @@ -149,6 +149,8 @@ import com.cloud.vm.VirtualMachine.Type; import com.cloud.vm.VirtualMachineManager; import com.cloud.vm.dao.NicDao; +import com.cloud.vm.dao.NicNetworkMapDao; +import com.cloud.vm.dao.NicNetworkMapVO; import com.cloud.vm.dao.NicSecondaryIpDao; import com.cloud.vm.dao.VMInstanceDao; @@ -232,6 +234,8 @@ public void setNetworkElements(List networkElements) { @Inject NicSecondaryIpDao _nicSecondaryIpDao; @Inject + NicNetworkMapDao _nicNetworkMapDao; + @Inject ApplicationLoadBalancerRuleDao _appLbRuleDao; @Inject private ProjectAccountDao _projectAccountDao; @@ -1027,6 +1031,23 @@ public Nic getNicInNetwork(long vmId, long networkId) { return _nicDao.findByNtwkIdAndInstanceId(networkId, vmId); } + @Override + public Pair getNicAndIpInNetwork(long vmId, long networkId) { + NicVO nic = _nicDao.findByNtwkIdAndInstanceId(networkId, vmId); + if (nic != null) { + return new Pair<>(nic, new Network.IpAddresses(nic.getIPv4Address(), nic.getIPv6Address())); + } + // networkId isn't any nic's primary - check whether it's one of a trunk nic's associated networks instead, + // where the guest ip lives in nic_network_map rather than on the nic itself + for (NicVO candidate : _nicDao.listByVmId(vmId)) { + NicNetworkMapVO association = _nicNetworkMapDao.findByNicIdAndNetworkId(candidate.getId(), networkId); + if (association != null) { + return new Pair<>(candidate, new Network.IpAddresses(association.getIp4Address(), association.getIp6Address())); + } + } + return null; + } + @Override public String getIpInNetwork(long vmId, long networkId) { Nic guestNic = getNicInNetwork(vmId, networkId); @@ -2349,6 +2370,12 @@ public List getUsedIpsInNetwork(Network network) { //Get ips used by tungsten List tfIps = tungstenGuestNetworkIpAddressDao.listGuestIpAddressByNetworkId(network.getId()); ips.addAll(tfIps); + //Get ips used by nics that are associated with this network as a secondary (trunk) network, not their primary + for (NicNetworkMapVO association : _nicNetworkMapDao.listByNetworkId(network.getId())) { + if (StringUtils.isNotBlank(association.getIp4Address())) { + ips.add(association.getIp4Address()); + } + } return ips; } diff --git a/server/src/main/java/com/cloud/network/NetworkServiceImpl.java b/server/src/main/java/com/cloud/network/NetworkServiceImpl.java index 2853fa96330d..feca688343cb 100644 --- a/server/src/main/java/com/cloud/network/NetworkServiceImpl.java +++ b/server/src/main/java/com/cloud/network/NetworkServiceImpl.java @@ -41,12 +41,17 @@ import javax.inject.Inject; import javax.naming.ConfigurationException; +import com.cloud.host.DetailVO; +import com.cloud.host.HostVO; import com.cloud.resourcelimit.CheckedReservation; import org.apache.cloudstack.acl.ControlledEntity.ACLType; import org.apache.cloudstack.acl.SecurityChecker.AccessType; import org.apache.cloudstack.alert.AlertService; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.command.admin.address.ReleasePodIpCmdByAdmin; +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.admin.network.CreateNetworkCmdByAdmin; import org.apache.cloudstack.api.command.admin.network.DedicateGuestVlanRangeCmd; import org.apache.cloudstack.api.command.admin.network.ListDedicatedGuestVlanRangesCmd; @@ -78,6 +83,7 @@ import org.apache.cloudstack.network.dao.NetworkPermissionDao; import org.apache.cloudstack.network.element.InternalLoadBalancerElementService; import org.apache.cloudstack.reservation.dao.ReservationDao; +import org.apache.cloudstack.storage.template.VnfTemplateManager; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.collections.MapUtils; import org.apache.commons.lang3.BooleanUtils; @@ -90,8 +96,10 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; +import com.cloud.agent.AgentManager; import com.cloud.agent.api.Answer; import com.cloud.agent.api.Command; +import com.cloud.agent.api.UpdateNicVlanMembershipCommand; import com.cloud.agent.api.to.IpAddressTO; import com.cloud.agent.api.to.NicTO; import com.cloud.agent.manager.Commands; @@ -126,6 +134,7 @@ import com.cloud.event.ActionEvent; import com.cloud.event.EventTypes; import com.cloud.event.UsageEventUtils; +import com.cloud.event.UsageEventVO; import com.cloud.exception.AccountLimitException; import com.cloud.exception.ConcurrentOperationException; import com.cloud.exception.InsufficientAddressCapacityException; @@ -137,7 +146,10 @@ import com.cloud.exception.UnsupportedServiceException; import com.cloud.host.Host; import com.cloud.host.dao.HostDao; +import com.cloud.host.dao.HostDetailsDao; import com.cloud.hypervisor.Hypervisor; +import com.cloud.hypervisor.HypervisorGuru; +import com.cloud.hypervisor.HypervisorGuruManager; import com.cloud.network.IpAddress.State; import com.cloud.network.Network.Capability; import com.cloud.network.Network.GuestType; @@ -214,6 +226,8 @@ import com.cloud.server.ResourceTag.ResourceObjectType; import com.cloud.service.ServiceOfferingVO; import com.cloud.service.dao.ServiceOfferingDao; +import com.cloud.storage.VMTemplateVO; +import com.cloud.storage.dao.VMTemplateDao; import com.cloud.tags.ResourceTagVO; import com.cloud.tags.dao.ResourceTagDao; import com.cloud.user.Account; @@ -226,6 +240,7 @@ import com.cloud.user.UserVO; import com.cloud.user.dao.AccountDao; import com.cloud.user.dao.UserDao; +import com.cloud.uservm.UserVm; import com.cloud.utils.Journal; import com.cloud.utils.NumbersUtil; import com.cloud.utils.Pair; @@ -266,6 +281,8 @@ import com.cloud.vm.VirtualMachineProfileImpl; import com.cloud.vm.dao.DomainRouterDao; import com.cloud.vm.dao.NicDao; +import com.cloud.vm.dao.NicNetworkMapDao; +import com.cloud.vm.dao.NicNetworkMapVO; import com.cloud.vm.dao.NicSecondaryIpDao; import com.cloud.vm.dao.NicSecondaryIpVO; import com.cloud.vm.dao.UserVmDao; @@ -329,6 +346,12 @@ public class NetworkServiceImpl extends ManagerBase implements NetworkService, C @Inject NicDao _nicDao = null; @Inject + NicNetworkMapDao _nicNetworkMapDao = null; + @Inject + AgentManager _agentMgr; + @Inject + HypervisorGuruManager _hvGuruMgr; + @Inject RulesManager _rulesMgr; List _networkGurus; @Inject @@ -374,6 +397,8 @@ public class NetworkServiceImpl extends ManagerBase implements NetworkService, C @Inject HostDao _hostDao; @Inject + HostDetailsDao _hostDetailsDao; + @Inject DataCenterVnetDao _dcVnetDao; @Inject AccountGuestVlanMapDao _accountGuestVlanMapDao; @@ -437,6 +462,10 @@ public class NetworkServiceImpl extends ManagerBase implements NetworkService, C private BGPService bgpService; @Inject private ASNumberDao asNumberDao; + @Inject + VMTemplateDao _templateDao; + @Inject + VnfTemplateManager vnfTemplateManager; List internalLoadBalancerElementServices = new ArrayList<>(); Map internalLoadBalancerElementServiceMap = new HashMap<>(); @@ -886,6 +915,582 @@ public boolean configureNicSecondaryIp(NicSecondaryIp secIp, boolean isZoneSgEna return success; } + @Override + @ActionEvent(eventType = EventTypes.EVENT_NIC_NETWORK_ASSOCIATE, eventDescription = "associating network to nic", create = true) + public Nic associateNetworkToNic(AssociateNetworkToNicCmd cmd) + throws ConcurrentOperationException, ResourceUnavailableException, InsufficientCapacityException, InsufficientAddressCapacityException { + NicVO nic = getTrunkEligibleNic(cmd.getNicId()); + VirtualMachine vm = getOwningVm(nic); + if (!NetworkOrchestrationService.MultiNetworkNicEnabled.valueIn(vm.getDataCenterId())) { + throw new InvalidParameterValueException(String.format("Multi-VLAN trunk NICs are not enabled in zone %s", vm.getDataCenterId())); + } + Account caller = CallContext.current().getCallingAccount(); + _accountMgr.checkAccess(caller, null, true, vm); + + boolean vmRunning = vm.getState() == VirtualMachine.State.Running; + if (vmRunning) { + checkHostReadyForVlanTrunk(vm); + } + + List newlyAssociatedNetworks = associateNetworksInternal(nic, vm, caller, cmd.getNetworkIds(), cmd.getIpAddressesMap()); + + if (vmRunning) { + updateLiveVlanTrunkMembership(vm, nic); + refreshNicVlanMappingMetadata(nic); + for (NetworkVO network : newlyAssociatedNetworks) { + pushDhcpEntryForAssociation(vm, nic, network); + publishAssociationUsageEvent(vm, nic, network, EventTypes.EVENT_NETWORK_OFFERING_ASSIGN); + } + } + + return _nicDao.findById(nic.getId()); + } + + /** + * Rejects disassociating a trunk nic's network while an active PF/Static NAT/LB rule still targets the + * association's allocated ip - mirrors the equivalent guard in releaseSecondaryIpFromNic for secondary ips. + */ + private void checkNoActiveRulesOnAssociation(long vmId, long networkId, String ip4Address) { + if (ip4Address == null) { + return; + } + List fwRulesList = _firewallDao.listByNetworkAndPurpose(networkId, Purpose.PortForwarding); + for (FirewallRuleVO rule : fwRulesList) { + if (_portForwardingDao.findByIdAndIp(rule.getId(), ip4Address) != null) { + throw new InvalidParameterValueException(String.format( + "Can't disassociate network %s, its allocated ip %s is associated with a port forwarding rule", networkId, ip4Address)); + } + } + IPAddressVO publicIpVO = _ipAddressDao.findByAssociatedVmIdAndVmIp(vmId, ip4Address); + if (publicIpVO != null) { + throw new InvalidParameterValueException(String.format( + "Can't disassociate network %s, its allocated ip %s is associated with static NAT rule public IP address ID: %s", networkId, ip4Address, publicIpVO)); + } + if (_loadBalancerDao.isLoadBalancerRulesMappedToVmGuestIp(vmId, ip4Address, networkId)) { + throw new InvalidParameterValueException(String.format( + "Can't disassociate network %s, its allocated ip %s is mapped to a load balancing rule", networkId, ip4Address)); + } + } + + @Override + @ActionEvent(eventType = EventTypes.EVENT_NIC_NETWORK_ASSOCIATE, eventDescription = "associating network to nic", create = true) + public void associateNetworksToNic(Nic nic, List networkIds, Map requestedIps) + throws ConcurrentOperationException, ResourceUnavailableException, InsufficientCapacityException, InsufficientAddressCapacityException { + NicVO nicVO = getTrunkEligibleNic(nic.getId()); + VirtualMachine vm = getOwningVm(nicVO); + if (!NetworkOrchestrationService.MultiNetworkNicEnabled.valueIn(vm.getDataCenterId())) { + throw new InvalidParameterValueException(String.format("Multi-VLAN trunk NICs are not enabled in zone %s", vm.getDataCenterId())); + } + if (vm.getState() != VirtualMachine.State.Stopped) { + throw new CloudRuntimeException(String.format( + "Nic %s's additional networks can only be associated while Instance %s is stopped or being created, not while %s", + nicVO.getUuid(), vm.getUuid(), vm.getState())); + } + Account caller = CallContext.current().getCallingAccount(); + _accountMgr.checkAccess(caller, null, true, vm); + associateNetworksInternal(nicVO, vm, caller, networkIds, requestedIps != null ? requestedIps : Collections.emptyMap()); + } + + private List associateNetworksInternal(NicVO nic, VirtualMachine vm, Account caller, List requestedNetworkIds, Map ipAddressesMap) + throws ResourceUnavailableException, InsufficientCapacityException, InsufficientAddressCapacityException, ConcurrentOperationException { + if (requestedNetworkIds == null || requestedNetworkIds.isEmpty()) { + throw new InvalidParameterValueException("networkids must not be empty"); + } + + NetworkVO primaryNetwork = _networksDao.findById(nic.getNetworkId()); + if (primaryNetwork == null) { + throw new CloudRuntimeException(String.format("Nic %s has no primary network", nic.getUuid())); + } + + VMTemplateVO template = _templateDao.findByIdIncludingRemoved(vm.getTemplateId()); + if (template != null) { + vnfTemplateManager.validateVnfApplianceTrunkNic(template, nic.getDeviceId()); + } + + Pair, List> networkSets = buildResultingNetworkSet(vm, nic, primaryNetwork, requestedNetworkIds, caller); + List resultingSet = networkSets.first(); + List requestedNetworks = networkSets.second(); + + validateTrunkSet(primaryNetwork, resultingSet); + validateNoOverlappingCidrs(resultingSet); + implementRequestedNetworks(requestedNetworks, vm, caller); + validateNoDuplicateVlanAfterImplement(resultingSet, requestedNetworks); + persistAssociations(nic, vm, requestedNetworks, ipAddressesMap); + return requestedNetworks; + } + + @Override + @ActionEvent(eventType = EventTypes.EVENT_NIC_NETWORK_DISASSOCIATE, eventDescription = "disassociating network from nic", create = true) + public Nic disassociateNetworkFromNic(DisassociateNetworkFromNicCmd cmd) throws ResourceUnavailableException { + NicVO nic = getTrunkEligibleNic(cmd.getNicId()); + VirtualMachine vm = getOwningVm(nic); + if (!NetworkOrchestrationService.MultiNetworkNicEnabled.valueIn(vm.getDataCenterId())) { + throw new InvalidParameterValueException(String.format("Multi-VLAN trunk NICs are not enabled in zone %s", vm.getDataCenterId())); + } + Account caller = CallContext.current().getCallingAccount(); + _accountMgr.checkAccess(caller, null, true, vm); + + long targetNetworkId = cmd.getNetworkId(); + if (targetNetworkId == nic.getNetworkId()) { + throw new InvalidParameterValueException(String.format( + "Network %s is nic %s's primary network and cannot be disassociated; add a new nic with the desired primary network instead", + targetNetworkId, nic.getUuid())); + } + + NicNetworkMapVO association = _nicNetworkMapDao.findByNicIdAndNetworkId(nic.getId(), targetNetworkId); + if (association == null) { + throw new InvalidParameterValueException(String.format("Nic %s is not associated with network %s", nic.getUuid(), targetNetworkId)); + } + checkNoActiveRulesOnAssociation(vm.getId(), targetNetworkId, association.getIp4Address()); + + boolean vmRunning = vm.getState() == VirtualMachine.State.Running; + if (vmRunning) { + checkHostReadyForVlanTrunk(vm); + } + + // guest IPs allocated for an association are never persisted anywhere but nic_network_map itself + // (allocateGuestIP/allocateGuestIpv6 compute availability fresh from live DB state each time), + // so removing this row is itself the release + _nicNetworkMapDao.remove(association.getId()); + + if (nic.getMultiNetwork() && _nicNetworkMapDao.listByNicId(nic.getId()).isEmpty()) { + nic.setMultiNetwork(false); + _nicDao.update(nic.getId(), nic); + } + + if (vmRunning) { + cleanupDhcpEntryForAssociation(vm, nic, targetNetworkId, association.getIp4Address(), association.getIp6Address()); + NetworkVO targetNetwork = _networksDao.findById(targetNetworkId); + if (targetNetwork != null) { + publishAssociationUsageEvent(vm, nic, targetNetwork, EventTypes.EVENT_NETWORK_OFFERING_REMOVE); + } + updateLiveVlanTrunkMembership(vm, nic); + refreshNicVlanMappingMetadata(nic); + } + + return _nicDao.findById(nic.getId()); + } + + @Override + @ActionEvent(eventType = EventTypes.EVENT_NIC_PRIMARY_NETWORK_CHANGE, eventDescription = "changing nic's primary network", create = true) + public Nic changeNicPrimaryNetwork(ChangeNicPrimaryNetworkCmd cmd) { + NicVO nic = getTrunkEligibleNic(cmd.getNicId()); + VirtualMachine vm = getOwningVm(nic); + if (!NetworkOrchestrationService.MultiNetworkNicEnabled.valueIn(vm.getDataCenterId())) { + throw new InvalidParameterValueException(String.format("Multi-VLAN trunk NICs are not enabled in zone %s", vm.getDataCenterId())); + } + Account caller = CallContext.current().getCallingAccount(); + _accountMgr.checkAccess(caller, null, true, vm); + + if (vm.getState() != VirtualMachine.State.Stopped) { + throw new InvalidParameterValueException(String.format( + "Nic %s's primary network can only be changed while Instance %s is stopped, since the nic is rebuilt fresh on its next start", + nic.getUuid(), vm.getUuid())); + } + + long newPrimaryNetworkId = cmd.getNetworkId(); + if (newPrimaryNetworkId == nic.getNetworkId()) { + throw new InvalidParameterValueException(String.format("Network %s is already nic %s's primary network", newPrimaryNetworkId, nic.getUuid())); + } + + NicNetworkMapVO association = _nicNetworkMapDao.findByNicIdAndNetworkId(nic.getId(), newPrimaryNetworkId); + if (association == null) { + throw new InvalidParameterValueException(String.format( + "Network %s must already be associated with nic %s before it can become the primary; associate it first", + newPrimaryNetworkId, nic.getUuid())); + } + + NetworkVO newPrimaryNetwork = _networksDao.findById(newPrimaryNetworkId); + if (newPrimaryNetwork == null) { + throw new CloudRuntimeException(String.format("Unable to find network %s", newPrimaryNetworkId)); + } + + long oldPrimaryNetworkId = nic.getNetworkId(); + String oldPrimaryIp4 = nic.getIPv4Address(); + String oldPrimaryIp6 = nic.getIPv6Address(); + + // reuses the IP already reserved for this network as an association - no new allocation needed, + // it's simply moving which table (nics vs nic_network_map) tracks the same reservation + _nicNetworkMapDao.remove(association.getId()); + _nicNetworkMapDao.persist(new NicNetworkMapVO(nic.getId(), oldPrimaryNetworkId, oldPrimaryIp4, oldPrimaryIp6)); + + nic.setNetworkId(newPrimaryNetworkId); + nic.setIPv4Address(association.getIp4Address()); + nic.setIPv6Address(association.getIp6Address()); + applyNetworkAddressingToNic(nic, newPrimaryNetwork); + _nicDao.update(nic.getId(), nic); + + return _nicDao.findById(nic.getId()); + } + + /** + * Refreshes a nic's network-derived fields (gateway, netmask, broadcast/isolation uri, IPv6 gateway/cidr) to + * match the given network. These are stored on the nic row itself rather than resolved live from networkId, so + * changeNicPrimaryNetwork must update them explicitly - GuestNetworkGuru.reserve() only refreshes + * broadcastUri/isolationUri on the VM's next start, and never touches gateway/netmask/IPv6 fields at all, + * which would otherwise stay wrong indefinitely. Mirrors the same field derivation GuestNetworkGuru.allocate() + * uses when a nic is first created against a network. + */ + private void applyNetworkAddressingToNic(NicVO nic, NetworkVO network) { + nic.setIPv4Gateway(network.getGateway()); + if (network.getCidr() != null) { + nic.setIPv4Netmask(NetUtils.cidr2Netmask(_networkModel.getValidNetworkCidr(network))); + } + nic.setBroadcastUri(network.getBroadcastUri()); + nic.setIsolationUri(network.getBroadcastUri()); + if (network.getIp6Cidr() != null && network.getIp6Gateway() != null) { + nic.setIPv6Cidr(network.getIp6Cidr()); + nic.setIPv6Gateway(network.getIp6Gateway()); + } + } + + // Pushes a DHCP entry for a newly-associated network on a Running Instance. + private void pushDhcpEntryForAssociation(VirtualMachine vm, NicVO nic, NetworkVO network) throws ResourceUnavailableException { + NicNetworkMapVO association = _nicNetworkMapDao.findByNicIdAndNetworkId(nic.getId(), network.getId()); + if (association == null) { + return; + } + sendDhcpEntryCommandForAssociation(vm, nic, network, association.getIp4Address(), association.getIp6Address(), false); + } + + // A live associate/disassociate on a Running Instance has no nic-plug/VM-start of its own to piggyback + // billing on, so without this the association would go unbilled until the VM's next stop/start. Bills + // only the one network just (dis)associated, never isDefault (an association is never a nic's default). + private void publishAssociationUsageEvent(VirtualMachine vm, NicVO nic, NetworkVO network, String eventType) { + UsageEventUtils.publishNicNetworkOfferingUsageEvent(eventType, vm.getAccountId(), vm.getDataCenterId(), vm.getId(), + VirtualMachine.class.getName(), vm.getUuid(), nic.getId(), network, 0L, vm.isDisplay()); + } + + // Counterpart to pushDhcpEntryForAssociation, for a Running Instance's disassociate. + private void cleanupDhcpEntryForAssociation(VirtualMachine vm, NicVO nic, long networkId, String ip4Address, String ip6Address) throws ResourceUnavailableException { + NetworkVO network = _networksDao.findById(networkId); + if (network == null) { + return; + } + sendDhcpEntryCommandForAssociation(vm, nic, network, ip4Address, ip6Address, true); + } + + // Bypasses the DhcpServiceProvider/NetworkElement pipeline: DhcpEntryRules re-fetches the nic's own DB row + // for its IP, which only ever holds the primary network's address, never an association's. + private void sendDhcpEntryCommandForAssociation(VirtualMachine vm, NicVO nic, NetworkVO network, String ip4Address, String ip6Address, boolean remove) + throws ResourceUnavailableException { + for (DomainRouterVO router : routerDao.findByNetwork(network.getId())) { + if (router.getState() != VirtualMachine.State.Running) { + continue; + } + Commands cmds = new Commands(Command.OnError.Stop); + commandSetupHelper.createDhcpEntryCommand(router, (UserVm) vm, nic.getMacAddress(), ip4Address, ip6Address, + network.getGateway(), network.getIp6Gateway(), network.getId(), false, remove, cmds); + networkHelper.sendCommandsToRouter(router, cmds); + } + } + + private void updateLiveVlanTrunkMembership(VirtualMachine vm, NicVO nic) throws ResourceUnavailableException { + if (vm.getHostId() == null) { + throw new CloudRuntimeException(String.format("Instance %s is Running but has no host assigned", vm.getUuid())); + } + DataCenter dc = _entityMgr.findById(DataCenter.class, vm.getDataCenterId()); + NicProfile nicProfile = _networkModel.getNicProfile(vm, nic, dc); + HypervisorGuru guru = _hvGuruMgr.getGuru(vm.getHypervisorType()); + NicTO nicTO = guru.toNicTO(nicProfile); + UpdateNicVlanMembershipCommand updateCmd = new UpdateNicVlanMembershipCommand(nicTO, vm.getInstanceName()); + Answer answer = _agentMgr.easySend(vm.getHostId(), updateCmd); + if (answer == null || !answer.getResult()) { + throw new CloudRuntimeException(String.format("Failed to update live VLAN trunk membership for nic %s: %s", + nic.getUuid(), answer != null ? answer.getDetails() : "no response from host")); + } + } + + /** + * Resends the nic's associated-network VLAN mapping metadata to its primary network's router(s), so the + * guest sees an up-to-date list immediately rather than only after that router's next restart. Best-effort: + * a failure here leaves guest-facing metadata briefly stale but must not fail the association change itself, + * since the actual nic_network_map/live VLAN membership updates (the correctness-critical parts) already + * succeeded by the time this runs. + */ + private void refreshNicVlanMappingMetadata(NicVO nic) { + UserVmVO vm = _userVmDao.findById(nic.getInstanceId()); + if (vm == null) { + return; + } + _userVmDao.loadDetails(vm); + List routers = routerDao.findByNetwork(nic.getNetworkId()); + for (DomainRouterVO router : routers) { + if (router.getState() != VirtualMachine.State.Running) { + continue; + } + try { + Commands cmds = new Commands(Command.OnError.Stop); + commandSetupHelper.createVmDataCommand(router, vm, nic, vm.getDetail("SSH.PublicKey"), cmds); + networkHelper.sendCommandsToRouter(router, cmds); + } catch (Exception e) { + logger.warn("Failed to refresh nic VLAN mapping metadata for nic {} on router {}: {}", nic.getUuid(), router.getUuid(), e.getMessage(), e); + } + } + } + + private NicVO getTrunkEligibleNic(long nicId) { + NicVO nic = _nicDao.findById(nicId); + if (nic == null) { + throw new InvalidParameterValueException(String.format("Unable to find nic with id %d", nicId)); + } + if (nic.getVmType() != VirtualMachine.Type.User) { + throw new InvalidParameterValueException("Multi-VLAN trunk nics are only supported on User Instances"); + } + return nic; + } + + private VirtualMachine getOwningVm(NicVO nic) { + VirtualMachine vm = _userVmDao.findById(nic.getInstanceId()); + if (vm == null) { + throw new InvalidParameterValueException(String.format("Unable to find the Instance owning nic %s", nic.getUuid())); + } + return vm; + } + + private Pair, List> buildResultingNetworkSet(VirtualMachine vm, NicVO nic, NetworkVO primaryNetwork, List requestedNetworkIds, Account caller) { + List existingAssociations = _nicNetworkMapDao.listByNicId(nic.getId()); + Set associatedNetworkIds = new HashSet<>(); + associatedNetworkIds.add(primaryNetwork.getId()); + List resultingSet = new ArrayList<>(); + resultingSet.add(primaryNetwork); + for (NicNetworkMapVO association : existingAssociations) { + associatedNetworkIds.add(association.getNetworkId()); + NetworkVO existingNetwork = _networksDao.findById(association.getNetworkId()); + if (existingNetwork != null) { + resultingSet.add(existingNetwork); + } + } + + Set networksOnOtherNics = getNetworksTouchedByOtherNics(vm, nic.getId()); + + List requestedNetworks = new ArrayList<>(); + for (Long requestedNetworkId : requestedNetworkIds) { + if (associatedNetworkIds.contains(requestedNetworkId)) { + NetworkVO alreadyAssociated = _networksDao.findById(requestedNetworkId); + throw new InvalidParameterValueException(String.format("Nic %s is already associated with network %s", nic.getUuid(), + alreadyAssociated != null ? alreadyAssociated.getUuid() : requestedNetworkId)); + } + if (networksOnOtherNics.contains(requestedNetworkId)) { + NetworkVO conflictingNetwork = _networksDao.findById(requestedNetworkId); + throw new InvalidParameterValueException(String.format( + "Instance %s already has network %s on a different nic; a network can only be reachable through one of an Instance's nics at a time", + vm.getUuid(), conflictingNetwork != null ? conflictingNetwork.getUuid() : requestedNetworkId)); + } + NetworkVO requestedNetwork = _networksDao.findById(requestedNetworkId); + if (requestedNetwork == null) { + throw new InvalidParameterValueException(String.format("Unable to find network with id %d", requestedNetworkId)); + } + _accountMgr.checkAccess(caller, null, true, requestedNetwork); + requestedNetworks.add(requestedNetwork); + resultingSet.add(requestedNetwork); + } + return new Pair<>(resultingSet, requestedNetworks); + } + + /** + * Networks already reachable through any of the vm's other nics - either as that nic's primary, or one of its + * own trunk associations. Mirrors VirtualMachineManagerImpl.checkIfNetworkExistsForUserVM's existing "at most + * one nic per network" rule for User VMs (trunk nics are already restricted to User Instances - see + * getTrunkEligibleNic), extended to also cover nic_network_map, which that check has no visibility into. + */ + private Set getNetworksTouchedByOtherNics(VirtualMachine vm, long excludingNicId) { + Set networkIds = new HashSet<>(); + for (NicVO otherNic : _nicDao.listByVmId(vm.getId())) { + if (otherNic.getId() == excludingNicId) { + continue; + } + networkIds.add(otherNic.getNetworkId()); + for (NicNetworkMapVO association : _nicNetworkMapDao.listByNicId(otherNic.getId())) { + networkIds.add(association.getNetworkId()); + } + } + return networkIds; + } + + private void validateTrunkSet(NetworkVO primaryNetwork, List resultingSet) { + for (NetworkVO network : resultingSet) { + validateNetworkFitsTrunk(primaryNetwork, network); + } + } + + /** + * A CIDR is a network's own fixed property (unlike its VLAN, which may still be auto-allocated at this + * point), so this can run before implementation, on the same resultingSet buildResultingNetworkSet already + * built. Two associated networks sharing address space is not a VLAN-isolation gap, but it still leaves a + * guest unable to unambiguously route between them - the same failure any multi-homed Linux host hits when + * two of its interfaces sit on identical subnets. + */ + private void validateNoOverlappingCidrs(List resultingSet) { + List> networkCidrs = new ArrayList<>(); + for (NetworkVO network : resultingSet) { + String cidr = network.getCidr(); + if (cidr == null) { + continue; + } + for (String range : cidr.split(",")) { + networkCidrs.add(new Pair<>(network, range.trim())); + } + } + for (int i = 0; i < networkCidrs.size(); i++) { + for (int j = i + 1; j < networkCidrs.size(); j++) { + NetworkVO networkA = networkCidrs.get(i).first(); + NetworkVO networkB = networkCidrs.get(j).first(); + if (networkA.getId() == networkB.getId()) { + continue; + } + String cidrA = networkCidrs.get(i).second(); + String cidrB = networkCidrs.get(j).second(); + if (NetUtils.isNetworksOverlap(cidrA, cidrB)) { + throw new InvalidParameterValueException(String.format( + "Networks %s (%s) and %s (%s) have overlapping subnets (%s and %s) and cannot be associated with the same nic; " + + "a guest cannot unambiguously route between associated networks that share address space", + networkA.getName(), networkA.getUuid(), networkB.getName(), networkB.getUuid(), cidrA, cidrB)); + } + } + } + } + + private void implementRequestedNetworks(List requestedNetworks, VirtualMachine vm, Account caller) + throws ConcurrentOperationException, ResourceUnavailableException, InsufficientCapacityException { + for (NetworkVO requestedNetwork : requestedNetworks) { + implementNetworkIfNeeded(requestedNetwork, vm, caller); + } + } + + private void validateNoDuplicateVlanAfterImplement(List resultingSet, List requestedNetworks) { + // re-fetch: implementNetworkIfNeeded may have just assigned a broadcastUri to one of the requested + // networks, and existing associations are re-read too rather than trusted stale + List implementedNetworks = new ArrayList<>(); + for (NetworkVO network : resultingSet) { + NetworkVO refreshed = _networksDao.findById(network.getId()); + if (refreshed == null) { + throw new CloudRuntimeException(String.format("Network %s disappeared during association", network.getUuid())); + } + implementedNetworks.add(refreshed); + } + Set justRequestedIds = new HashSet<>(); + for (NetworkVO network : requestedNetworks) { + justRequestedIds.add(network.getId()); + } + validateNoDuplicateVlan(implementedNetworks, justRequestedIds); + } + + private void persistAssociations(NicVO nic, VirtualMachine vm, List requestedNetworks, Map requestedIps) + throws InsufficientAddressCapacityException { + Map safeRequestedIps = requestedIps != null ? requestedIps : Collections.emptyMap(); + for (NetworkVO network : requestedNetworks) { + NetworkVO refreshed = _networksDao.findById(network.getId()); + Pair allocatedIps = allocateIpsForAssociation(refreshed, safeRequestedIps.get(network.getId())); + _nicNetworkMapDao.persist(new NicNetworkMapVO(nic.getId(), network.getId(), allocatedIps.first(), allocatedIps.second())); + } + if (!nic.getMultiNetwork()) { + nic.setMultiNetwork(true); + _nicDao.update(nic.getId(), nic); + // this nic's own still-open usage row(s) predate network_id existing on this table and were never + // backfilled in bulk; backfill them now, unconditionally, since a later association on this same + // nic could otherwise introduce an offering collision this row can no longer be told apart from. + // Published as a usage event rather than written directly: this request thread already has an + // ambient `cloud` DB transaction open, and cloud_usage is a separate database - the usage job + // consumes this event and performs the actual write on its own thread, same as every other + // usage_network_offering row in this feature. + Map details = Collections.singletonMap(UsageEventVO.DynamicParameters.networkId.name(), String.valueOf(nic.getNetworkId())); + UsageEventUtils.publishUsageEvent(EventTypes.EVENT_NIC_NETWORK_ID_BACKFILL, vm.getAccountId(), vm.getDataCenterId(), nic.getId(), + Long.toString(nic.getId()), null, null, null, VirtualMachine.class.getName(), vm.getUuid(), details); + } + } + + private Pair allocateIpsForAssociation(NetworkVO network, Network.IpAddresses requested) throws InsufficientAddressCapacityException { + if (network.getGuestType() == Network.GuestType.L2) { + return new Pair<>(null, null); + } + String requestedIp4 = requested != null ? requested.getIp4Address() : null; + String requestedIp6 = requested != null ? requested.getIp6Address() : null; + String ip4Address = _ipAddrMgr.allocateGuestIP(network, requestedIp4); + String ip6Address = StringUtils.isNotBlank(network.getIp6Cidr()) ? ipv6AddrMgr.allocateGuestIpv6(network, requestedIp6) : null; + return new Pair<>(ip4Address, ip6Address); + } + + private void checkHostReadyForVlanTrunk(VirtualMachine vm) { + if (vm.getHostId() == null) { + throw new CloudRuntimeException(String.format("Instance %s is Running but has no host assigned", vm.getUuid())); + } + DetailVO vlanFilteringDetail = _hostDetailsDao.findDetail(vm.getHostId(), Host.HOST_VLAN_FILTERING_ENABLED); + if (vlanFilteringDetail == null || !Boolean.parseBoolean(vlanFilteringDetail.getValue())) { + HostVO host = _hostDao.findById(vm.getHostId()); + throw new InvalidParameterValueException(String.format( + "Host %s does not have VLAN filtering enabled on its guest bridge, so a multi-VLAN trunk nic cannot be associated on this Instance while it is running", + host != null ? host.getUuid() : vm.getHostId())); + } + } + + private void implementNetworkIfNeeded(NetworkVO network, VirtualMachine vm, Account caller) + throws ConcurrentOperationException, ResourceUnavailableException, InsufficientCapacityException { + if (network.getState() == Network.State.Implemented) { + return; + } + DataCenter dc = _entityMgr.findById(DataCenter.class, network.getDataCenterId()); + Host host = vm.getHostId() != null ? _hostDao.findById(vm.getHostId()) : null; + DeployDestination dest = new DeployDestination(dc, null, null, host); + User callerUser = _accountMgr.getActiveUser(CallContext.current().getCallingUserId()); + ReservationContext context = new ReservationContextImpl(null, null, callerUser, caller); + Pair implemented = _networkMgr.implementNetwork(network.getId(), dest, context); + if (implemented == null || implemented.first() == null) { + throw new CloudRuntimeException(String.format("Failed to implement network %s as part of associating it to a nic", network.getUuid())); + } + } + + private void validateNoDuplicateVlan(List resultingSet, Set justRequestedIds) { + Map seenBroadcastUris = new HashMap<>(); + for (NetworkVO network : resultingSet) { + URI broadcastUri = network.getBroadcastUri(); + if (broadcastUri == null) { + if (!justRequestedIds.contains(network.getId())) { + // this call never attempted to implement this network (it's the nic's existing primary, + // untouched here and only implemented later at the VM's actual start; or an association + // that already existed before this call) - whatever reason it currently lacks a VLAN is + // unrelated to this request. It isn't this method's job to flag that; a genuinely missing + // VLAN at actual nic-plug time already fails closed with its own clear error there. + continue; + } + throw new CloudRuntimeException(String.format("Network %s (%s) has no VLAN assigned after implementation; this should not happen", + network.getName(), network.getUuid())); + } + NetworkVO conflicting = seenBroadcastUris.put(broadcastUri, network); + if (conflicting != null) { + throw new InvalidParameterValueException(String.format("Networks %s and %s share the same VLAN (%s) and cannot be associated with the same nic", + conflicting.getUuid(), network.getUuid(), broadcastUri)); + } + } + } + + private void validateNetworkFitsTrunk(NetworkVO primaryNetwork, NetworkVO network) { + if (network.getGuestType() != primaryNetwork.getGuestType()) { + throw new InvalidParameterValueException(String.format( + "All networks associated with one nic must be the same type; network %s is %s but the nic's primary network %s is %s", + network.getUuid(), network.getGuestType(), primaryNetwork.getUuid(), primaryNetwork.getGuestType())); + } + if (!Objects.equals(network.getPhysicalNetworkId(), primaryNetwork.getPhysicalNetworkId())) { + throw new InvalidParameterValueException(String.format( + "All networks associated with one nic must be on the same physical network; network %s is not on the same physical network as %s", + network.getUuid(), primaryNetwork.getUuid())); + } + if (!Objects.equals(network.getDataCenterId(), primaryNetwork.getDataCenterId())) { + throw new InvalidParameterValueException(String.format( + "All networks associated with one nic must be in the same zone; network %s is not in the same zone as %s", + network.getUuid(), primaryNetwork.getUuid())); + } + if (network.getBroadcastDomainType() != BroadcastDomainType.Vlan) { + throw new InvalidParameterValueException(String.format("Multi-VLAN trunk nics only support VLAN-isolated networks; network %s is %s", + network.getUuid(), network.getBroadcastDomainType())); + } + if (_networkModel.isSecurityGroupSupportedInNetwork(network)) { + throw new InvalidParameterValueException(String.format( + "Network %s has security groups enabled; a nic associated with multiple networks may not use a security-group-enabled network", + network.getUuid())); + } + } + /** * It allocates a secondary IP alias on the NIC. It can be either an Ipv4 or an Ipv6 or even both, according to the given IpAddresses object. */ @@ -3583,12 +4188,20 @@ public void doInTransactionWithoutResult(TransactionStatus status) { updateNetworkIpv6(network, networkOfferingId); network.setNetworkOfferingId(networkOfferingId); _networksDao.update(networkId, network, newSvcProviders); - // get all nics using this network + // get all nics using this network, either as their primary network or as an + // associated network of a multi-VLAN trunk nic // log remove usage events for old offering // log assign usage events for new offering + Set nicIds = new HashSet<>(); List nics = _nicDao.listByNetworkId(networkId); for (NicVO nic : nics) { - if (Nic.ReservationStrategy.PlaceHolder.equals(nic.getReservationStrategy())) { + nicIds.add(nic.getId()); + } + nicIds.addAll(_nicNetworkMapDao.listNicIdsByNetworkId(networkId)); + Map usageDetails = Collections.singletonMap(UsageEventVO.DynamicParameters.networkId.name(), String.valueOf(networkId)); + for (Long nicId : nicIds) { + NicVO nic = _nicDao.findById(nicId); + if (nic == null || Nic.ReservationStrategy.PlaceHolder.equals(nic.getReservationStrategy())) { continue; } long vmId = nic.getInstanceId(); @@ -3599,10 +4212,10 @@ public void doInTransactionWithoutResult(TransactionStatus status) { } long isDefault = (nic.isDefaultNic()) ? 1 : 0; String nicIdString = Long.toString(nic.getId()); - UsageEventUtils.publishUsageEvent(EventTypes.EVENT_NETWORK_OFFERING_REMOVE, vm.getAccountId(), vm.getDataCenterId(), vm.getId(), nicIdString, oldNetworkOfferingId, - null, isDefault, VirtualMachine.class.getName(), vm.getUuid(), vm.isDisplay()); - UsageEventUtils.publishUsageEvent(EventTypes.EVENT_NETWORK_OFFERING_ASSIGN, vm.getAccountId(), vm.getDataCenterId(), vm.getId(), nicIdString, networkOfferingId, - null, isDefault, VirtualMachine.class.getName(), vm.getUuid(), vm.isDisplay()); + UsageEventUtils.publishUsageEventWithDetails(EventTypes.EVENT_NETWORK_OFFERING_REMOVE, vm.getAccountId(), vm.getDataCenterId(), vm.getId(), nicIdString, oldNetworkOfferingId, + null, isDefault, VirtualMachine.class.getName(), vm.getUuid(), usageDetails, vm.isDisplay()); + UsageEventUtils.publishUsageEventWithDetails(EventTypes.EVENT_NETWORK_OFFERING_ASSIGN, vm.getAccountId(), vm.getDataCenterId(), vm.getId(), nicIdString, networkOfferingId, + null, isDefault, VirtualMachine.class.getName(), vm.getUuid(), usageDetails, vm.isDisplay()); } } }); diff --git a/server/src/main/java/com/cloud/network/lb/LoadBalancingRulesManagerImpl.java b/server/src/main/java/com/cloud/network/lb/LoadBalancingRulesManagerImpl.java index 38f405fe1e4d..b0be0df4e0b0 100644 --- a/server/src/main/java/com/cloud/network/lb/LoadBalancingRulesManagerImpl.java +++ b/server/src/main/java/com/cloud/network/lb/LoadBalancingRulesManagerImpl.java @@ -977,8 +977,7 @@ public void updateLBHealthChecks(Scheme scheme) throws ResourceUnavailableExcept List lbVmMaps = _lb2VmMapDao.listByLoadBalancerId(ulb.getId()); for (LoadBalancerVMMapVO lbVmMap : lbVmMaps) { UserVm vm = _vmDao.findById(lbVmMap.getInstanceId()); - Nic nic = _nicDao.findByInstanceIdAndNetworkIdIncludingRemoved(ulb.getNetworkId(), vm.getId()); - String dstIp = lbVmMap.getInstanceIp() == null ? nic.getIPv4Address(): lbVmMap.getInstanceIp(); + String dstIp = lbVmMap.getInstanceIp() == null ? getLbDestinationIp(vm.getId(), ulb.getNetworkId()) : lbVmMap.getInstanceIp(); for (int i = 0; i < lbto.getDestinations().length; i++) { LoadBalancerTO.DestinationTO des = lbto.getDestinations()[i]; @@ -1097,9 +1096,10 @@ public boolean assignToLoadBalancer(long loadBalancerId, List instanceIds, Network loadBalancerNetwork = _networkDao.findById(loadBalancer.getNetworkId()); _accountMgr.checkAccess(vmOwner, SecurityChecker.AccessType.UseEntry, false, loadBalancerNetwork); - Nic vmNicInLb = getVmNicInLoadBalancer(vm, loadBalancer, loadBalancerNetwork, vmIdNetworkMap, vmOwner); + Pair vmNicAndIpInLb = getVmNicInLoadBalancer(vm, loadBalancer, loadBalancerNetwork, vmIdNetworkMap, vmOwner); + Nic vmNicInLb = vmNicAndIpInLb.first(); - String priIp = vmNicInLb.getIPv4Address(); + String priIp = vmNicAndIpInLb.second().getIp4Address(); if (existingVmIdIps.containsKey(instanceId)) { // now check for ip address @@ -1123,13 +1123,14 @@ public boolean assignToLoadBalancer(long loadBalancerId, List instanceIds, if (vmIpsList != null) { - //check if the ips belongs to nic secondary ip + //check if the ips belongs to nic secondary ip - scoped by network, since the same nic can carry + //secondary ips across more than one of its associated networks for (String ip: vmIpsList) { // skip the primary ip from vm secondary ip comparisions if (ip.equals(priIp)) { continue; } - if(_nicSecondaryIpDao.findByIp4AddressAndNicId(ip,vmNicInLb.getId()) == null) { + if(_nicSecondaryIpDao.findByIp4AddressAndNetworkIdAndInstanceId(loadBalancer.getNetworkId(), instanceId, ip) == null) { throw new InvalidParameterValueException("Instance IP "+ ip + " specified does not belong to " + "NIC in Network " + vmNicInLb.getNetworkId()); } @@ -1221,11 +1222,11 @@ public void doInTransactionWithoutResult(TransactionStatus status) { return success; } - protected Nic getVmNicInLoadBalancer(UserVm vm, LoadBalancerVO loadBalancer, Network loadBalancerNetwork, Map vmIdNetworkMap, Account vmOwner) { + protected Pair getVmNicInLoadBalancer(UserVm vm, LoadBalancerVO loadBalancer, Network loadBalancerNetwork, Map vmIdNetworkMap, Account vmOwner) { boolean isVpcConserveModeEnabled = _vpcMgr.isNetworkOnVpcEnabledConserveMode(loadBalancerNetwork); boolean isNetworkPassedVpcConserveMode = isVpcConserveModeEnabled && MapUtils.isNotEmpty(vmIdNetworkMap) && vmIdNetworkMap.containsKey(vm.getId()); - Nic vmNicInLb = isNetworkPassedVpcConserveMode ? + Pair vmNicInLb = isNetworkPassedVpcConserveMode ? getNicForVmInVpcConserveModeTierNetwork(vm, vmIdNetworkMap, vmOwner, loadBalancerNetwork) : getNicForVmLbNetwork(vm, loadBalancer); @@ -1241,29 +1242,40 @@ protected Nic getVmNicInLoadBalancer(UserVm vm, LoadBalancerVO loadBalancer, Net } /** - * For Isolated Networks or Network tiers of VPCs not using Conserve mode, use the same network as the load balancer - * @return the nic of the VM in the load balancer network + * For Isolated Networks or Network tiers of VPCs not using Conserve mode, use the same network as the load balancer - + * either as the vm's primary nic, or (for a trunk nic) one of its associated networks. + * @return the nic and guest ip of the VM in the load balancer network */ - protected Nic getNicForVmLbNetwork(UserVm vm, LoadBalancerVO loadBalancer) { - List nics = _networkModel.getNics(vm.getId()); - for (Nic nic : nics) { - if (nic.getNetworkId() == loadBalancer.getNetworkId()) { - return nic; - } - } - return null; + protected Pair getNicForVmLbNetwork(UserVm vm, LoadBalancerVO loadBalancer) { + return _networkModel.getNicAndIpInNetwork(vm.getId(), loadBalancer.getNetworkId()); } /** * On VPC Conserve Mode, VMs from multiple VPC networks tiers can be assigned to the same load balancer. - * @return the nic of the VM in the specified tier network in `vmIdNetworkMap` + * @return the nic and guest ip of the VM in the specified tier network in `vmIdNetworkMap` */ - protected Nic getNicForVmInVpcConserveModeTierNetwork(UserVm vm, Map vmIdNetworkMap, Account vmOwner, Network loadBalancerNetwork) { + protected Pair getNicForVmInVpcConserveModeTierNetwork(UserVm vm, Map vmIdNetworkMap, Account vmOwner, Network loadBalancerNetwork) { Long vmNetworkId = vmIdNetworkMap.get(vm.getId()); Network vmNetwork = _networkDao.findById(vmNetworkId); _accountMgr.checkAccess(vmOwner, SecurityChecker.AccessType.UseEntry, false, vmNetwork); checkNetworkBelongsToLoadBalancerVpc(vmNetwork, loadBalancerNetwork); - return _networkModel.getNicInNetwork(vm.getId(), vmNetworkId); + return _networkModel.getNicAndIpInNetwork(vm.getId(), vmNetworkId); + } + + /** + * Resolves the guest ip a VM answers on for a LB rule's network - the nic's own primary network match when one + * exists, or a trunk nic's association (from nic_network_map) otherwise. Falls back further to a since-removed + * nic's last-known address (matching this method's replaced call sites' original use of + * NicDao.findByInstanceIdAndNetworkIdIncludingRemoved) so a VM whose nic was unplugged after being added to + * this LB rule still resolves to its last-known ip, same as before. Returns null only if none of these match. + */ + private String getLbDestinationIp(long vmId, long networkId) { + Pair nicAndIp = _networkModel.getNicAndIpInNetwork(vmId, networkId); + if (nicAndIp != null) { + return nicAndIp.second().getIp4Address(); + } + Nic removedNic = _nicDao.findByInstanceIdAndNetworkIdIncludingRemoved(networkId, vmId); + return removedNic != null ? removedNic.getIPv4Address() : null; } protected void checkNetworkBelongsToLoadBalancerVpc(Network vmNetwork, Network loadBalancerNetwork) { @@ -2277,8 +2289,7 @@ public List getExistingDestinations(long lbId) { String dstIp = null; for (LoadBalancerVMMapVO lbVmMap : lbVmMaps) { UserVm vm = _vmDao.findById(lbVmMap.getInstanceId()); - Nic nic = _nicDao.findByInstanceIdAndNetworkIdIncludingRemoved(lb.getNetworkId(), vm.getId()); - dstIp = lbVmMap.getInstanceIp() == null ? nic.getIPv4Address(): lbVmMap.getInstanceIp(); + dstIp = lbVmMap.getInstanceIp() == null ? getLbDestinationIp(vm.getId(), lb.getNetworkId()) : lbVmMap.getInstanceIp(); LbDestination lbDst = new LbDestination(lb.getDefaultPortStart(), lb.getDefaultPortEnd(), dstIp, lbVmMap.isRevoke()); dstList.add(lbDst); } @@ -2754,9 +2765,10 @@ public Map getLbInstances(long lbId) { for (LoadBalancerVMMapVO lbVmMap : lbVmMaps) { UserVm vm = _vmDao.findById(lbVmMap.getInstanceId()); - Nic nic = _nicDao.findByInstanceIdAndNetworkIdIncludingRemoved(lb.getNetworkId(), vm.getId()); - Ip ip = new Ip(nic.getIPv4Address()); - dstList.put(ip, vm); + String ip4Address = getLbDestinationIp(vm.getId(), lb.getNetworkId()); + if (ip4Address != null) { + dstList.put(new Ip(ip4Address), vm); + } } return dstList; } 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 58b2892e09c3..9875e33be366 100644 --- a/server/src/main/java/com/cloud/network/router/CommandSetupHelper.java +++ b/server/src/main/java/com/cloud/network/router/CommandSetupHelper.java @@ -157,7 +157,10 @@ import com.cloud.vm.dao.NicDao; import com.cloud.vm.dao.NicIpAliasDao; import com.cloud.vm.dao.NicIpAliasVO; +import com.cloud.vm.dao.NicNetworkMapDao; +import com.cloud.vm.dao.NicNetworkMapVO; import com.cloud.vm.dao.UserVmDao; +import com.google.gson.Gson; public class CommandSetupHelper { @@ -171,6 +174,8 @@ public class CommandSetupHelper { @Inject private NicDao _nicDao; @Inject + private NicNetworkMapDao _nicNetworkMapDao; + @Inject private NetworkDao _networkDao; @Inject private DomainRouterDao _routerDao; @@ -251,6 +256,8 @@ public void createVmDataCommand(final VirtualRouter router, final UserVm vm, fin vmDataCommand.addVmData(NetworkModel.METATDATA_DIR, NetworkModel.CLOUD_NAME_FILE, customCloudName); } + addNicVlanMappingToVmData(vmDataCommand, vm, nic); + cmds.addCommand("vmdata", vmDataCommand); } } @@ -277,23 +284,38 @@ public void createApplyVpnUsersCommand(final List users, fina } public void createDhcpEntryCommand(final VirtualRouter router, final UserVm vm, final NicVO nic, boolean remove, final Commands cmds) { - final DhcpEntryCommand dhcpCommand = new DhcpEntryCommand(nic.getMacAddress(), nic.getIPv4Address(), vm.getHostName(), nic.getIPv6Address(), - _networkModel.getExecuteInSeqNtwkElmtCmd()); + createDhcpEntryCommand(router, vm, nic.getMacAddress(), nic.getIPv4Address(), nic.getIPv6Address(), nic.getIPv4Gateway(), nic.getIPv6Gateway(), + nic.getNetworkId(), nic.isDefaultNic(), remove, false, cmds); + } - String gatewayIp = nic.getIPv4Gateway(); + // 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. 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()); final DataCenterVO dcVo = _dcDao.findById(router.getDataCenterId()); - dhcpCommand.setDefaultRouter(gatewayIp); - dhcpCommand.setIp6Gateway(nic.getIPv6Gateway()); + dhcpCommand.setDefaultRouter(ip4Gateway); + dhcpCommand.setIp6Gateway(ip6Gateway); String ipaddress = null; final NicVO domrDefaultNic = findDefaultDnsIp(vm.getId()); if (domrDefaultNic != null) { ipaddress = domrDefaultNic.getIPv4Address(); } dhcpCommand.setDefaultDns(ipaddress); - dhcpCommand.setDuid(NetUtils.getDuidLL(nic.getMacAddress())); - dhcpCommand.setDefault(nic.isDefaultNic()); + dhcpCommand.setDuid(NetUtils.getDuidLL(macAddress)); + dhcpCommand.setDefault(isDefaultNic); + dhcpCommand.setAssociatedNetwork(isAssociatedNetwork); dhcpCommand.setRemove(remove); // Set DHCP lease timeout from zone-scoped config (0 = infinite) @@ -302,7 +324,7 @@ public void createDhcpEntryCommand(final VirtualRouter router, final UserVm vm, dhcpCommand.setAccessDetail(NetworkElementCommand.ROUTER_IP, _routerControlHelper.getRouterControlIp(router.getId())); dhcpCommand.setAccessDetail(NetworkElementCommand.ROUTER_NAME, router.getInstanceName()); - dhcpCommand.setAccessDetail(NetworkElementCommand.ROUTER_GUEST_IP, _routerControlHelper.getRouterIpInNetwork(nic.getNetworkId(), router.getId())); + dhcpCommand.setAccessDetail(NetworkElementCommand.ROUTER_GUEST_IP, _routerControlHelper.getRouterIpInNetwork(networkId, router.getId())); dhcpCommand.setAccessDetail(NetworkElementCommand.ZONE_NETWORK_TYPE, dcVo.getNetworkType().toString()); cmds.addCommand("dhcp", dhcpCommand); @@ -819,6 +841,36 @@ public void createDhcpEntryCommandsForVMs(final DomainRouterVO router, final Com createDhcpEntryCommand(router, vm, nic, false, cmds); } } + createDhcpEntryCommandsForAssociatedNics(router, cmds, guestNetworkId, dc, dnsBasicZoneUpdates); + } + + // A nic whose primary network is something else but is also associated with guestNetworkId (multi-VLAN + // trunk nic) is invisible to the primary-only lookup above, so a full VR rebuild would otherwise drop its + // entry - it would keep its current lease but fail to renew once this VR is the only one it can ask. + private void createDhcpEntryCommandsForAssociatedNics(final DomainRouterVO router, final Commands cmds, final long guestNetworkId, + final DataCenterVO dc, final String dnsBasicZoneUpdates) { + final NetworkVO network = _networkDao.findById(guestNetworkId); + if (network == null) { + return; + } + for (final NicNetworkMapVO association : _nicNetworkMapDao.listByNetworkId(guestNetworkId)) { + final NicVO nic = _nicDao.findById(association.getNicId()); + if (nic == null) { + continue; + } + final UserVmVO vm = _userVmDao.findById(nic.getInstanceId()); + if (vm == null || !(vm.getState() == VirtualMachine.State.Running || vm.getState() == VirtualMachine.State.Migrating + || vm.getState() == VirtualMachine.State.Stopping)) { + continue; + } + if (dc.getNetworkType() == NetworkType.Basic && router.getPodIdToDeployIn().longValue() != vm.getPodIdToDeployIn().longValue() + && dnsBasicZoneUpdates.equalsIgnoreCase("pod")) { + continue; + } + logger.debug("Creating dhcp entry for vm " + vm + "'s association with network " + guestNetworkId + " on domR " + router + "."); + createDhcpEntryCommand(router, vm, nic.getMacAddress(), association.getIp4Address(), association.getIp6Address(), + network.getGateway(), network.getIp6Gateway(), guestNetworkId, false, false, cmds); + } } public void createDeleteIpAliasCommand(final DomainRouterVO router, final List deleteIpAliasTOs, final List createIpAliasTos, final long networkId, @@ -1362,6 +1414,38 @@ protected void addUserDataDetailsToCommand(VmDataCommand cmd, String userDataDet } } + protected void addNicVlanMappingToVmData(VmDataCommand cmd, UserVm vm, NicVO nic) { + if (!nic.getMultiNetwork() || !VirtualMachineManager.AllowExposeNicVlanMapping.valueIn(vm.getAccountId())) { + return; + } + List associations = _nicNetworkMapDao.listByNicId(nic.getId()); + List mappings = new ArrayList<>(); + for (NicNetworkMapVO association : associations) { + NetworkVO associatedNetwork = _networkDao.findById(association.getNetworkId()); + if (associatedNetwork == null || associatedNetwork.getBroadcastDomainType() != BroadcastDomainType.Vlan || associatedNetwork.getBroadcastUri() == null) { + continue; + } + String vlanTag = BroadcastDomainType.Vlan.getValueFrom(associatedNetwork.getBroadcastUri()); + mappings.add(new NicVlanMappingEntry(associatedNetwork.getUuid(), associatedNetwork.getName(), vlanTag)); + } + // empty-string data is vmdata.py's signal to delete a previously-written file: once a nic has been + // trunked, its last association being removed must actively clear any stale file left from before, + // not just skip writing - the guest must never see associations that no longer exist + cmd.addVmData(NetworkModel.METATDATA_DIR, NetworkModel.NIC_VLAN_MAPPING_FILE, mappings.isEmpty() ? "" : new Gson().toJson(mappings)); + } + + private static final class NicVlanMappingEntry { + private final String networkId; + private final String networkName; + private final String vlan; + + private NicVlanMappingEntry(String networkId, String networkName, String vlan) { + this.networkId = networkId; + this.networkName = networkName; + this.vlan = vlan; + } + } + private NicVO findGatewayIp(final long userVmId) { final NicVO defaultNic = _nicDao.findDefaultNicForVM(userVmId); return defaultNic; diff --git a/server/src/main/java/com/cloud/network/rules/RulesManagerImpl.java b/server/src/main/java/com/cloud/network/rules/RulesManagerImpl.java index bb3ee8d76248..b79d357c7fe6 100644 --- a/server/src/main/java/com/cloud/network/rules/RulesManagerImpl.java +++ b/server/src/main/java/com/cloud/network/rules/RulesManagerImpl.java @@ -276,21 +276,23 @@ public PortForwardingRule createPortForwardingRule(final PortForwardingRule rule throw new InvalidParameterValueException(String.format("Invalid user vm: %s", vm)); } - // Verify that vm has nic in the network + // Verify that vm has nic in the network - either as its primary, or (for a trunk nic) as one of + // its associated networks, whose guest ip lives in nic_network_map rather than on the nic itself Ip dstIp = rule.getDestinationIpAddress(); - guestNic = _networkModel.getNicInNetwork(vmId, networkId); - if (guestNic == null || guestNic.getIPv4Address() == null) { + Pair nicAndIp = _networkModel.getNicAndIpInNetwork(vmId, networkId); + if (nicAndIp == null || nicAndIp.second().getIp4Address() == null) { throw new InvalidParameterValueException("Vm doesn't belong to network associated with ipAddress"); - } else { - dstIp = new Ip(guestNic.getIPv4Address()); } + guestNic = nicAndIp.first(); + dstIp = new Ip(nicAndIp.second().getIp4Address()); if (vmIp != null) { //vm ip is passed so it can be primary or secondary ip addreess. if (!dstIp.equals(vmIp)) { //the vm ip is secondary ip to the nic. - // is vmIp is secondary ip or not - NicSecondaryIp secondaryIp = _nicSecondaryDao.findByIp4AddressAndNicId(vmIp.toString(), guestNic.getId()); + // is vmIp is secondary ip or not - scoped by network, since the same nic can carry + // secondary ips across more than one of its associated networks + NicSecondaryIp secondaryIp = _nicSecondaryDao.findByIp4AddressAndNetworkIdAndInstanceId(networkId, vmId, vmIp.toString()); if (secondaryIp == null) { throw new InvalidParameterValueException("IP Address is not in the VM nic's network "); } @@ -476,12 +478,14 @@ private boolean enableStaticNat(long ipId, long vmId, long networkId, boolean is throw new InvalidParameterValueException("Unable to find network by id"); } - // Check that vm has a nic in the network - guestNic = _networkModel.getNicInNetwork(vmId, networkId); - if (guestNic == null) { + // Check that vm has a nic in the network - either as its primary, or (for a trunk nic) as one of its + // associated networks, whose guest ip lives in nic_network_map rather than on the nic itself + Pair nicAndIp = _networkModel.getNicAndIpInNetwork(vmId, networkId); + if (nicAndIp == null) { throw new InvalidParameterValueException("Vm doesn't belong to the network with specified id"); } - dstIp = guestNic.getIPv4Address(); + guestNic = nicAndIp.first(); + dstIp = nicAndIp.second().getIp4Address(); if (!_networkModel.areServicesSupportedInNetwork(network.getId(), Service.StaticNat)) { throw new InvalidParameterValueException("Unable to create static nat rule; StaticNat service is not " + "supported in network with specified id"); @@ -573,8 +577,9 @@ private boolean enableStaticNat(long ipId, long vmId, long networkId, boolean is if (!secondaryIpSet) { throw new InvalidParameterValueException("VM ip " + vmGuestIp + " address not belongs to the vm"); } - //check the ip belongs to the vm or not - nicSecIp = _nicSecondaryDao.findByIp4AddressAndNicId(vmGuestIp, guestNic.getId()); + //check the ip belongs to the vm or not - scoped by network, since the same nic can carry + //secondary ips across more than one of its associated networks + nicSecIp = _nicSecondaryDao.findByIp4AddressAndNetworkIdAndInstanceId(networkId, vmId, vmGuestIp); if (nicSecIp == null) { throw new InvalidParameterValueException("VM ip " + vmGuestIp + " address not belongs to the vm"); } @@ -1479,9 +1484,10 @@ protected List createStaticNatForIp(IpAddress sourceIp, Account calle } // create new static nat rule - // Get nic IP4 address + // Get nic IP4 address - a null primary-nic match doesn't necessarily mean the vm has no nic in this + // network; it may be a trunk nic's associated (non-primary) network instead Nic guestNic = _networkModel.getNicInNetworkIncludingRemoved(vm.getId(), networkId); - if (guestNic == null) { + if (guestNic == null && _networkModel.getNicAndIpInNetwork(vm.getId(), networkId) == null) { throw new InvalidParameterValueException("Vm doesn't belong to the network with specified id"); } @@ -1659,20 +1665,22 @@ public PortForwardingRule updatePortForwardingRule(UpdatePortForwardingRuleCmd c } Ip dstIp = rule.getDestinationIpAddress(); if (virtualMachineId != null) { - // Verify that vm has nic in the network - Nic guestNic = _networkModel.getNicInNetwork(virtualMachineId, rule.getNetworkId()); - if (guestNic == null || guestNic.getIPv4Address() == null) { + // Verify that vm has nic in the network - either as its primary, or (for a trunk nic) as one of its + // associated networks, whose guest ip lives in nic_network_map rather than on the nic itself + Pair nicAndIp = _networkModel.getNicAndIpInNetwork(virtualMachineId, rule.getNetworkId()); + if (nicAndIp == null || nicAndIp.second().getIp4Address() == null) { throw new InvalidParameterValueException("Vm doesn't belong to network associated with ipAddress"); - } else { - dstIp = new Ip(guestNic.getIPv4Address()); } + Nic guestNic = nicAndIp.first(); + dstIp = new Ip(nicAndIp.second().getIp4Address()); if (vmGuestIp != null) { //vm ip is passed so it can be primary or secondary ip addreess. if (!dstIp.equals(vmGuestIp)) { //the vm ip is secondary ip to the nic. - // is vmIp is secondary ip or not - NicSecondaryIp secondaryIp = _nicSecondaryDao.findByIp4AddressAndNicId(vmGuestIp.toString(), guestNic.getId()); + // is vmIp is secondary ip or not - scoped by network, since the same nic can carry + // secondary ips across more than one of its associated networks + NicSecondaryIp secondaryIp = _nicSecondaryDao.findByIp4AddressAndNetworkIdAndInstanceId(rule.getNetworkId(), virtualMachineId, vmGuestIp.toString()); if (secondaryIp == null) { throw new InvalidParameterValueException("IP Address is not in the VM nic's network "); } diff --git a/server/src/main/java/com/cloud/server/ManagementServerImpl.java b/server/src/main/java/com/cloud/server/ManagementServerImpl.java index f32857d7cf04..282cd082e972 100644 --- a/server/src/main/java/com/cloud/server/ManagementServerImpl.java +++ b/server/src/main/java/com/cloud/server/ManagementServerImpl.java @@ -133,7 +133,10 @@ import org.apache.cloudstack.api.command.admin.management.RemoveManagementServerCmd; import org.apache.cloudstack.api.command.admin.network.AddNetworkDeviceCmd; import org.apache.cloudstack.api.command.admin.network.AddNetworkServiceProviderCmd; +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.CloneNetworkOfferingCmd; +import org.apache.cloudstack.api.command.admin.network.DisassociateNetworkFromNicCmd; import org.apache.cloudstack.api.command.admin.network.CreateManagementNetworkIpRangeCmd; import org.apache.cloudstack.api.command.admin.network.CreateNetworkCmdByAdmin; import org.apache.cloudstack.api.command.admin.network.CreateNetworkOfferingCmd; @@ -1652,6 +1655,8 @@ public ExcludeList applyAffinityConstraints(VirtualMachine vm, VirtualMachinePro } } + _dpMgr.avoidHostsNotReadyForMultiNetworkNics(vmProfile, _dcDao.findById(plan.getDataCenterId()), excludes); + // call affinitygroup chain final long vmGroupCount = _affinityGroupVMMapDao.countAffinityGroupsForVm(vm.getId()); @@ -1761,6 +1766,20 @@ public Ternary, Integer>, List, Map(otherHosts, suitableHosts, requiresStorageMotion); } + @Override + public String getMultiNetworkNicUnsuitableReason(final VirtualMachine vm, final Host host) { + boolean vmHasMultiNetworkNic = nicDao.listByVmId(vm.getId()).stream().anyMatch(NicVO::getMultiNetwork); + if (!vmHasMultiNetworkNic) { + return null; + } + final DetailVO detail = _detailsDao.findDetail(host.getId(), Host.HOST_VLAN_FILTERING_ENABLED); + final boolean vlanFilteringEnabled = detail != null && Boolean.parseBoolean(detail.getValue()); + if (!vlanFilteringEnabled) { + return "Instance has a multi-VLAN trunk NIC, but this host does not have VLAN filtering enabled"; + } + return null; + } + private void validateVgpuProfileForVmMigration(final VirtualMachineProfile vmProfile) { // Validate if the VM is using a vGPU profile that supports migration. ServiceOffering serviceOffering = vmProfile.getServiceOffering(); @@ -4355,6 +4374,9 @@ public List> getCommands() { cmdList.add(ListPublicIpAddressesCmdByAdmin.class); cmdList.add(CreateNetworkCmdByAdmin.class); cmdList.add(UpdateNetworkCmdByAdmin.class); + cmdList.add(AssociateNetworkToNicCmd.class); + cmdList.add(DisassociateNetworkFromNicCmd.class); + cmdList.add(ChangeNicPrimaryNetworkCmd.class); cmdList.add(ListNetworksCmdByAdmin.class); cmdList.add(CreateVPCCmdByAdmin.class); cmdList.add(ListVPCsCmdByAdmin.class); diff --git a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java index 9bbbdc503508..c323e9dd7b6b 100644 --- a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java +++ b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java @@ -39,6 +39,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.Comparator; import java.util.Date; import java.util.HashMap; import java.util.HashSet; @@ -270,10 +271,12 @@ import com.cloud.exception.VirtualMachineMigrationException; import com.cloud.gpu.GPU; import com.cloud.ha.HighAvailabilityManager; +import com.cloud.host.DetailVO; import com.cloud.host.Host; import com.cloud.host.HostVO; import com.cloud.host.Status; import com.cloud.host.dao.HostDao; +import com.cloud.host.dao.HostDetailsDao; import com.cloud.hypervisor.Hypervisor; import com.cloud.hypervisor.Hypervisor.HypervisorType; import com.cloud.hypervisor.dao.HypervisorCapabilitiesDao; @@ -422,6 +425,8 @@ import com.cloud.vm.dao.InstanceGroupVMMapDao; import com.cloud.vm.dao.NicDao; import com.cloud.vm.dao.NicExtraDhcpOptionDao; +import com.cloud.vm.dao.NicNetworkMapDao; +import com.cloud.vm.dao.NicNetworkMapVO; import com.cloud.vm.dao.UserVmDao; import com.cloud.vm.dao.VMInstanceDao; import com.cloud.vm.dao.VMInstanceDetailsDao; @@ -452,6 +457,8 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir @Inject private HostDao _hostDao; @Inject + private HostDetailsDao _hostDetailsDao; + @Inject private ServiceOfferingDao serviceOfferingDao; @Inject private DiskOfferingDao _diskOfferingDao; @@ -524,6 +531,8 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir @Inject private NicDao _nicDao; @Inject + private NicNetworkMapDao _nicNetworkMapDao; + @Inject private RulesManager _rulesMgr; @Inject private LoadBalancingRulesManager _lbMgr; @@ -1836,14 +1845,6 @@ public UserVm updateDefaultNicForVirtualMachine(UpdateDefaultNicForVMCmd cmd) th throw new CloudRuntimeException("Failed to find a NIC profile for the existing default Network. This is bad and probably means some sort of configuration corruption"); } - Network oldDefaultNetwork = null; - oldDefaultNetwork = _networkModel.getDefaultNetworkForVm(vmId); - String oldNicIdString = Long.toString(_networkModel.getDefaultNic(vmId).getId()); - long oldNetworkOfferingId = -1L; - - if (oldDefaultNetwork != null) { - oldNetworkOfferingId = oldDefaultNetwork.getNetworkOfferingId(); - } NicVO existingVO = _nicDao.findById(existing.id); int chosenID = nic.getDeviceId(); Integer existingID = existing.getDeviceId(); @@ -1869,16 +1870,16 @@ public UserVm updateDefaultNicForVirtualMachine(UpdateDefaultNicForVMCmd cmd) th throw new CloudRuntimeException("Failed to change default nic to " + nic + " and now we have no default"); } else if (newdefault.getId() == nic.getNetworkId()) { logger.debug("successfully set default network to " + network + " for " + vmInstance); - String nicIdString = Long.toString(nic.getId()); - long newNetworkOfferingId = network.getNetworkOfferingId(); - UsageEventUtils.publishUsageEvent(EventTypes.EVENT_NETWORK_OFFERING_REMOVE, vmInstance.getAccountId(), vmInstance.getDataCenterId(), vmInstance.getId(), - oldNicIdString, oldNetworkOfferingId, null, 1L, VirtualMachine.class.getName(), vmInstance.getUuid(), vmInstance.isDisplay()); - UsageEventUtils.publishUsageEvent(EventTypes.EVENT_NETWORK_OFFERING_ASSIGN, vmInstance.getAccountId(), vmInstance.getDataCenterId(), vmInstance.getId(), nicIdString, - newNetworkOfferingId, null, 1L, VirtualMachine.class.getName(), vmInstance.getUuid(), vmInstance.isDisplay()); - UsageEventUtils.publishUsageEvent(EventTypes.EVENT_NETWORK_OFFERING_REMOVE, vmInstance.getAccountId(), vmInstance.getDataCenterId(), vmInstance.getId(), nicIdString, - newNetworkOfferingId, null, 0L, VirtualMachine.class.getName(), vmInstance.getUuid(), vmInstance.isDisplay()); - UsageEventUtils.publishUsageEvent(EventTypes.EVENT_NETWORK_OFFERING_ASSIGN, vmInstance.getAccountId(), vmInstance.getDataCenterId(), vmInstance.getId(), - oldNicIdString, oldNetworkOfferingId, null, 0L, VirtualMachine.class.getName(), vmInstance.getUuid(), vmInstance.isDisplay()); + // close nic's old row before opening its new one, not after - otherwise the REMOVE below can't tell + // the stale row apart from the one just ASSIGNed and closes both + UsageEventUtils.publishNicNetworkOfferingUsageEvents(EventTypes.EVENT_NETWORK_OFFERING_REMOVE, vmInstance.getAccountId(), vmInstance.getDataCenterId(), + vmInstance.getId(), VirtualMachine.class.getName(), vmInstance.getUuid(), existingVO, 1L, vmInstance.isDisplay()); + UsageEventUtils.publishNicNetworkOfferingUsageEvents(EventTypes.EVENT_NETWORK_OFFERING_REMOVE, vmInstance.getAccountId(), vmInstance.getDataCenterId(), + vmInstance.getId(), VirtualMachine.class.getName(), vmInstance.getUuid(), nic, 0L, vmInstance.isDisplay()); + UsageEventUtils.publishNicNetworkOfferingUsageEvents(EventTypes.EVENT_NETWORK_OFFERING_ASSIGN, vmInstance.getAccountId(), vmInstance.getDataCenterId(), + vmInstance.getId(), VirtualMachine.class.getName(), vmInstance.getUuid(), nic, 1L, vmInstance.isDisplay()); + UsageEventUtils.publishNicNetworkOfferingUsageEvents(EventTypes.EVENT_NETWORK_OFFERING_ASSIGN, vmInstance.getAccountId(), vmInstance.getDataCenterId(), + vmInstance.getId(), VirtualMachine.class.getName(), vmInstance.getUuid(), existingVO, 0L, vmInstance.isDisplay()); if (vmInstance.getState() == State.Running) { try { @@ -3290,10 +3291,9 @@ private void saveUsageEvent(UserVmVO vm) { private void generateNetworkUsageForVm(VirtualMachine vm, boolean isDisplay, String eventType) { List nics = _nicDao.listByVmId(vm.getId()); for (NicVO nic : nics) { - NetworkVO network = _networkDao.findById(nic.getNetworkId()); long isDefault = (nic.isDefaultNic()) ? 1 : 0; - UsageEventUtils.publishUsageEvent(eventType, vm.getAccountId(), vm.getDataCenterId(), vm.getId(), - Long.toString(nic.getId()), network.getNetworkOfferingId(), null, isDefault, vm.getClass().getName(), vm.getUuid(), isDisplay); + UsageEventUtils.publishNicNetworkOfferingUsageEvents(eventType, vm.getAccountId(), vm.getDataCenterId(), vm.getId(), + vm.getClass().getName(), vm.getUuid(), nic, isDefault, isDisplay); } } @@ -3491,6 +3491,37 @@ protected void updateUserData(UserVm vm) throws ResourceUnavailableException, In } } + // Converges a trunk nic's associated-network DHCP entries with its current nic_network_map rows on start; + // no-op for a nic without associations. + void syncNicNetworkAssociationDhcpEntries(UserVmVO vm, NicVO nic) { + for (NicNetworkMapVO association : _nicNetworkMapDao.listByNicId(nic.getId())) { + sendAssociationDhcpEntry(vm, nic, association.getNetworkId(), association.getIp4Address(), association.getIp6Address(), false); + } + for (NicNetworkMapVO removedAssociation : _nicNetworkMapDao.listRemovedByNicId(nic.getId())) { + sendAssociationDhcpEntry(vm, nic, removedAssociation.getNetworkId(), removedAssociation.getIp4Address(), removedAssociation.getIp6Address(), true); + } + } + + private void sendAssociationDhcpEntry(UserVmVO vm, NicVO nic, long networkId, String ip4Address, String ip6Address, boolean remove) { + NetworkVO network = _networkDao.findById(networkId); + if (network == null) { + return; + } + for (DomainRouterVO router : _routerDao.findByNetwork(networkId)) { + if (router.getState() != State.Running) { + continue; + } + try { + Commands commands = new Commands(Command.OnError.Stop); + commandSetupHelper.createDhcpEntryCommand(router, vm, nic.getMacAddress(), ip4Address, ip6Address, + network.getGateway(), network.getIp6Gateway(), networkId, false, remove, commands); + nwHelper.sendCommandsToRouter(router, commands); + } catch (ResourceUnavailableException e) { + logger.warn("Failed to sync DHCP entry for nic {} on associated network {}: {}", nic.getUuid(), networkId, e.getMessage(), e); + } + } + } + private void updateDns(UserVmVO vm, String hostName) throws ResourceUnavailableException, InsufficientCapacityException { if (!StringUtils.isEmpty(hostName)) { vm.setHostName(hostName); @@ -5809,8 +5840,9 @@ public boolean finalizeStart(VirtualMachineProfile profile, long hostId, Command for (NicVO nic : nics) { NetworkVO network = _networkDao.findById(nic.getNetworkId()); long isDefault = (nic.isDefaultNic()) ? 1 : 0; - UsageEventUtils.publishUsageEvent(EventTypes.EVENT_NETWORK_OFFERING_ASSIGN, vm.getAccountId(), vm.getDataCenterId(), vm.getId(), Long.toString(nic.getId()), - network.getNetworkOfferingId(), null, isDefault, VirtualMachine.class.getName(), vm.getUuid(), vm.isDisplay()); + UsageEventUtils.publishNicNetworkOfferingUsageEvents(EventTypes.EVENT_NETWORK_OFFERING_ASSIGN, vm.getAccountId(), vm.getDataCenterId(), vm.getId(), + VirtualMachine.class.getName(), vm.getUuid(), nic, isDefault, vm.isDisplay()); + syncNicNetworkAssociationDhcpEntries(vm, nic); if (network.getTrafficType() == TrafficType.Guest) { originalIp = nic.getIPv4Address(); guestNic = nic; @@ -6631,6 +6663,7 @@ private void verifyTemplate(BaseDeployVMCmd cmd, VirtualMachineTemplate template if (TemplateType.VNF.equals(template.getTemplateType())) { if (!_itMgr.isBlankInstance(template)) { vnfTemplateManager.validateVnfApplianceNics(template, cmd.getNetworkIds(), cmd.getVmNetworkMap()); + vnfTemplateManager.validateVnfApplianceTrunkNics(template, cmd.getNicNetworksList()); } } else if (cmd instanceof DeployVnfApplianceCmd) { throw new InvalidParameterValueException("Can't deploy VNF appliance from a non-VNF template"); @@ -6858,6 +6891,8 @@ private UserVm createVirtualMachine(BaseDeployVMCmd cmd, DataCenter zone, Accoun } } + persistAdditionalNicNetworkAssociations(cmd, vm); + // check if this templateId has a child ISO List child_templates = _templateDao.listByParentTemplatetId(template.getId()); for (VMTemplateVO tmpl: child_templates) { @@ -6901,6 +6936,63 @@ private UserVm createVirtualMachine(BaseDeployVMCmd cmd, DataCenter zone, Accoun return vm; } + /** + * Persists nicnetworkslist's additional (non-primary) networks into nic_network_map for each newly-created nic, + * including any per-network requested IPs (nicnetworkslist[N].ip4addresses/ip6addresses) - a network with no + * requested IP auto-allocates, same as before this was added. The primary network of each nicnetworkslist entry + * already flows through the ordinary networkIds/ipToNetworkMap pipeline above unchanged, so this only handles + * the associations that pipeline has no concept of. No live agent update is issued - the vm hasn't started yet, + * so its first plug already reads these associations fresh. + */ + /** + * Mirrors the check {@link #addAdditionalNetworksToVm} already runs for a nic's primary network, extended to + * an associated network as well - carrying a trunk nic's associations across an account move must not grant + * the new account reachability into a network it otherwise couldn't use. + */ + private void checkNewAccountAccessToAssociatedNetworks(List nicNetworksList, Account newAccount) { + for (BaseDeployVMCmd.NicNetworkGrouping grouping : nicNetworksList) { + for (Long networkId : grouping.getAssociatedNetworkIds()) { + NetworkVO network = _networkDao.findById(networkId); + if (network == null) { + throw new InvalidParameterValueException("Unable to find specified Network ID: " + networkId); + } + _networkModel.checkNetworkPermissions(newAccount, network); + } + } + } + + private void persistAdditionalNicNetworkAssociations(BaseDeployVMCmd cmd, UserVm vm) + throws InsufficientCapacityException, ResourceUnavailableException, ConcurrentOperationException { + persistAdditionalNicNetworkAssociations(cmd.getNicNetworksList(), vm); + } + + /** + * Persists a nicnetworkslist-shaped grouping's additional (non-primary) networks into nic_network_map for + * each of the vm's current nics, matched to a grouping by ascending device id. Used both at deploy time + * (nics freshly created) and by {@link #moveVmToUser} (nics freshly rebuilt by the account reassignment), + * so a trunk nic's associations can be carried across either without being silently flattened. + */ + private void persistAdditionalNicNetworkAssociations(List nicNetworksList, UserVm vm) + throws InsufficientCapacityException, ResourceUnavailableException, ConcurrentOperationException { + if (CollectionUtils.isEmpty(nicNetworksList)) { + return; + } + List nics = _nicDao.listByVmId(vm.getId()); + nics.sort(Comparator.comparingInt(NicVO::getDeviceId)); + if (nics.size() != nicNetworksList.size()) { + throw new CloudRuntimeException(String.format( + "Instance %s has %d nic(s) but %d %s entries were requested", + vm.getUuid(), nics.size(), nicNetworksList.size(), ApiConstants.NIC_NETWORKS_LIST)); + } + for (int i = 0; i < nics.size(); i++) { + BaseDeployVMCmd.NicNetworkGrouping grouping = nicNetworksList.get(i); + List additionalNetworkIds = grouping.getAssociatedNetworkIds(); + if (!additionalNetworkIds.isEmpty()) { + networkService.associateNetworksToNic(nics.get(i), additionalNetworkIds, grouping.getAssociatedNetworkIps()); + } + } + } + protected void validateLeaseProperties(Integer leaseDuration, VMLeaseManager.ExpiryAction leaseExpiryAction) { if (ObjectUtils.allNull(leaseDuration, leaseExpiryAction) // both are null || (leaseDuration != null && leaseDuration == -1)) { // special condition to disable lease for instance @@ -7659,6 +7751,11 @@ private DeployDestination checkVmMigrationDestination(VMInstanceVO vm, Host srcH throw new CloudRuntimeException("Cannot migrate VM, VM is DPDK enabled VM but destination host is not DPDK enabled"); } + if (!isHostReadyForMultiNetworkNics(vm, destinationHost)) { + throw new InvalidParameterValueException(String.format( + "Cannot migrate VM, VM has a multi-VLAN trunk nic but destination host: %s does not have VLAN filtering enabled", destinationHost)); + } + HostVO destinationHostVO = _hostDao.findById(destinationHost.getId()); _hostDao.loadHostTags(destinationHostVO); validateStrictHostTagCheck(vm, destinationHostVO); @@ -7690,6 +7787,16 @@ private DeployDestination checkVmMigrationDestination(VMInstanceVO vm, Host srcH return dest; } + // A multi-VLAN trunk nic can only be plugged on a host with VLAN filtering enabled - true (ready) for a VM with no such nic. + boolean isHostReadyForMultiNetworkNics(VMInstanceVO vm, Host host) { + boolean hasMultiNetworkNic = _nicDao.listByVmId(vm.getId()).stream().anyMatch(NicVO::getMultiNetwork); + if (!hasMultiNetworkNic) { + return true; + } + DetailVO detail = _hostDetailsDao.findDetail(host.getId(), Host.HOST_VLAN_FILTERING_ENABLED); + return detail != null && Boolean.parseBoolean(detail.getValue()); + } + private boolean isOnSupportedHypevisorForMigration(VMInstanceVO vm) { return (vm.getHypervisorType().equals(HypervisorType.XenServer) || vm.getHypervisorType().equals(HypervisorType.VMware) || @@ -8251,6 +8358,7 @@ public UserVm moveVmToUser(final AssignVMCmd cmd) throws ResourceAllocationExcep Network newNetwork = null; if (!cmd.isSkipNetwork()) { newNetwork = ensureDestinationNetwork(cmd, vm, newAccount); + implementAssociatedNetworksForOwnershipChange(cmd.getNicNetworksList(), vm, caller); } try { Transaction.execute(new TransactionCallbackNoReturn() { @@ -8418,6 +8526,31 @@ protected Network ensureDestinationNetwork(AssignVMCmd cmd, UserVmVO vm, Account return createApplicableNetworkToCreateVm(newAccount, zone); } + /** + * Implements (deploys a virtual router for, if one doesn't already exist) every network a trunk nic will be + * associated with as part of this move, same as {@link #createApplicableNetworkToCreateVm} already does for a + * freshly-created primary network. Must run here, before the ownership-change transaction opens: implementing + * a network can start a virtual router, which blocks the calling thread on an async VM-work-job outcome that + * can never be observed while this thread is holding that transaction open. + */ + protected void implementAssociatedNetworksForOwnershipChange(List nicNetworksList, UserVmVO vm, Account caller) { + if (CollectionUtils.isEmpty(nicNetworksList)) { + return; + } + DataCenterVO zone = _dcDao.findById(vm.getDataCenterId()); + for (BaseDeployVMCmd.NicNetworkGrouping grouping : nicNetworksList) { + for (Long networkId : grouping.getAssociatedNetworkIds()) { + NetworkVO network = _networkDao.findById(networkId); + if (network == null) { + throw new InvalidParameterValueException("Unable to find specified Network ID: " + networkId); + } + if (network.getState() != Network.State.Implemented) { + implementNetwork(caller, zone, network); + } + } + } + } + /** * @return a network offering with required availability that will be used to create a new isolated network for the VM * assignment process. @@ -8478,7 +8611,11 @@ protected void executeStepsToChangeOwnershipOfVm(AssignVMCmd cmd, Account caller try { updateVmNetwork(cmd, caller, vm, newAccount, template); - } catch (InsufficientCapacityException | ResourceAllocationException e) { + if (!cmd.isSkipNetwork()) { + checkNewAccountAccessToAssociatedNetworks(cmd.getNicNetworksList(), newAccount); + persistAdditionalNicNetworkAssociations(cmd.getNicNetworksList(), vm); + } + } catch (InsufficientCapacityException | ResourceAllocationException | ResourceUnavailableException | ConcurrentOperationException e) { throw new CloudRuntimeException(String.format("Unable to update networks when assigning VM [%s] due to [%s].", vm, e.getMessage()), e); } diff --git a/server/src/main/java/com/cloud/vm/UserVmStateListener.java b/server/src/main/java/com/cloud/vm/UserVmStateListener.java index aa1805d3366e..971ae69803f0 100644 --- a/server/src/main/java/com/cloud/vm/UserVmStateListener.java +++ b/server/src/main/java/com/cloud/vm/UserVmStateListener.java @@ -36,7 +36,6 @@ import com.cloud.event.UsageEventUtils; import com.cloud.event.dao.UsageEventDao; import com.cloud.network.dao.NetworkDao; -import com.cloud.network.dao.NetworkVO; import com.cloud.server.ManagementService; import com.cloud.service.dao.ServiceOfferingDao; import com.cloud.utils.component.ComponentContext; @@ -99,9 +98,8 @@ public boolean postStateTransitionEvent(StateMachine2.Transition t generateUsageEvent(vo.getServiceOfferingId(), vo, EventTypes.EVENT_VM_STOP); List nics = _nicDao.listByVmId(vo.getId()); for (NicVO nic : nics) { - NetworkVO network = _networkDao.findById(nic.getNetworkId()); - UsageEventUtils.publishUsageEvent(EventTypes.EVENT_NETWORK_OFFERING_REMOVE, vo.getAccountId(), vo.getDataCenterId(), vo.getId(), - Long.toString(nic.getId()), network.getNetworkOfferingId(), null, 0L, vo.getClass().getName(), vo.getUuid(), vo.isDisplay()); + UsageEventUtils.publishNicNetworkOfferingUsageEvents(EventTypes.EVENT_NETWORK_OFFERING_REMOVE, vo.getAccountId(), vo.getDataCenterId(), vo.getId(), + vo.getClass().getName(), vo.getUuid(), nic, 0L, vo.isDisplay()); } } else if (newState == State.Destroyed || newState == State.Error || newState == State.Expunging) { generateUsageEvent(vo.getServiceOfferingId(), vo, EventTypes.EVENT_VM_DESTROY); diff --git a/server/src/main/java/org/apache/cloudstack/storage/template/VnfTemplateManagerImpl.java b/server/src/main/java/org/apache/cloudstack/storage/template/VnfTemplateManagerImpl.java index 0ebff237a44e..a21f5a7b7277 100644 --- a/server/src/main/java/org/apache/cloudstack/storage/template/VnfTemplateManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/storage/template/VnfTemplateManagerImpl.java @@ -76,6 +76,7 @@ import org.apache.cloudstack.api.command.user.template.ListVnfTemplatesCmd; import org.apache.cloudstack.api.command.user.template.RegisterVnfTemplateCmd; import org.apache.cloudstack.api.command.user.template.UpdateVnfTemplateCmd; +import org.apache.cloudstack.api.command.user.vm.BaseDeployVMCmd; import org.apache.cloudstack.api.command.user.vm.DeployVnfApplianceCmd; import org.apache.cloudstack.api.command.user.vm.ListVnfAppliancesCmd; import org.apache.cloudstack.framework.config.ConfigKey; @@ -220,6 +221,32 @@ public void validateVnfApplianceNics(VirtualMachineTemplate template, List } } + @Override + public void validateVnfApplianceTrunkNics(VirtualMachineTemplate template, List nicNetworksList) { + if (CollectionUtils.isEmpty(nicNetworksList)) { + return; + } + List vnfNics = vnfTemplateNicDao.listByTemplateId(template.getId()); + for (VnfTemplateNicVO vnfNic : vnfNics) { + if (!vnfNic.isManagement() || vnfNic.getDeviceId() >= nicNetworksList.size()) { + continue; + } + if (!nicNetworksList.get((int) vnfNic.getDeviceId()).getAssociatedNetworkIds().isEmpty()) { + throw new InvalidParameterValueException("VNF nic is the management interface and cannot be a multi-VLAN trunk nic: " + vnfNic); + } + } + } + + @Override + public void validateVnfApplianceTrunkNic(VirtualMachineTemplate template, long deviceId) { + List vnfNics = vnfTemplateNicDao.listByTemplateId(template.getId()); + for (VnfTemplateNicVO vnfNic : vnfNics) { + if (vnfNic.isManagement() && vnfNic.getDeviceId() == deviceId) { + throw new InvalidParameterValueException("VNF nic is the management interface and cannot be a multi-VLAN trunk nic: " + vnfNic); + } + } + } + private void validateVnfApplianceNetworksMap(VirtualMachineTemplate template, Map vmNetworkMap) { if (MapUtils.isEmpty(vmNetworkMap)) { throw new InvalidParameterValueException("VNF networks map is empty"); diff --git a/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java b/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java index 840a1de45962..39aeb406091a 100644 --- a/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java @@ -1044,9 +1044,8 @@ private void publishVMUsageUpdateResourceCount(final UserVm userVm, ServiceOffer List nics = nicDao.listByVmId(userVm.getId()); for (NicVO nic : nics) { try { - NetworkVO network = networkDao.findById(nic.getNetworkId()); - UsageEventUtils.publishUsageEvent(EventTypes.EVENT_NETWORK_OFFERING_ASSIGN, userVm.getAccountId(), userVm.getDataCenterId(), userVm.getId(), - Long.toString(nic.getId()), network.getNetworkOfferingId(), null, 1L, VirtualMachine.class.getName(), userVm.getUuid(), userVm.isDisplay()); + UsageEventUtils.publishNicNetworkOfferingUsageEvents(EventTypes.EVENT_NETWORK_OFFERING_ASSIGN, userVm.getAccountId(), userVm.getDataCenterId(), + userVm.getId(), VirtualMachine.class.getName(), userVm.getUuid(), nic, 1L, userVm.isDisplay()); } catch (Exception e) { logger.error(String.format("Failed to publish network usage records during VM import. %s", StringUtils.defaultString(e.getMessage()))); } diff --git a/server/src/test/java/com/cloud/deploy/DeploymentPlanningManagerImplTest.java b/server/src/test/java/com/cloud/deploy/DeploymentPlanningManagerImplTest.java index e77fdaee500c..e7f59eadeb8b 100644 --- a/server/src/test/java/com/cloud/deploy/DeploymentPlanningManagerImplTest.java +++ b/server/src/test/java/com/cloud/deploy/DeploymentPlanningManagerImplTest.java @@ -80,6 +80,8 @@ import com.cloud.vm.VirtualMachine.Type; import com.cloud.vm.VirtualMachineProfile; import com.cloud.vm.VirtualMachineProfileImpl; +import com.cloud.vm.NicVO; +import com.cloud.vm.dao.NicDao; import com.cloud.vm.dao.UserVmDao; import com.cloud.vm.dao.VMInstanceDetailsDao; import com.cloud.vm.dao.VMInstanceDao; @@ -130,6 +132,7 @@ import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; @@ -228,6 +231,9 @@ public class DeploymentPlanningManagerImplTest { @Mock DataStoreManager _dataStoreManager; + @Mock + NicDao nicDao; + @Inject HostPodDao _podDao; @@ -267,6 +273,7 @@ public void testSetUp() { Mockito.when(vmDetailsDao.listDetailsKeyPairs(ArgumentMatchers.anyLong())).thenReturn(null); Mockito.when(volDao.findByInstance(ArgumentMatchers.anyLong())).thenReturn(new ArrayList<>()); + Mockito.when(nicDao.listByVmId(ArgumentMatchers.anyLong())).thenReturn(new ArrayList<>()); Mockito.when(_dcDao.findById(ArgumentMatchers.anyLong())).thenReturn(dc); Mockito.when(dc.getId()).thenReturn(dataCenterId); @@ -476,6 +483,53 @@ public void avoidDisabledDataCentersTest() { } } + @Test + public void avoidHostsNotReadyForMultiNetworkNicsNoTrunkNic() { + VirtualMachineProfile vmProfile = Mockito.mock(VirtualMachineProfile.class); + Mockito.when(vmProfile.getId()).thenReturn(instanceId); + DataCenter dc = Mockito.mock(DataCenter.class); + Mockito.when(dc.getId()).thenReturn(dataCenterId); + ExcludeList avoids = new ExcludeList(); + + NicVO ordinaryNic = Mockito.mock(NicVO.class); + Mockito.when(ordinaryNic.getMultiNetwork()).thenReturn(false); + Mockito.when(nicDao.listByVmId(instanceId)).thenReturn(Arrays.asList(ordinaryNic)); + + _dpm.avoidHostsNotReadyForMultiNetworkNics(vmProfile, dc, avoids); + + assertAvoidIsEmpty(avoids, true, true, true, true); + Mockito.verify(hostDao, Mockito.never()).listEnabledIdsByDataCenterId(Mockito.anyLong()); + } + + @Test + public void avoidHostsNotReadyForMultiNetworkNicsExcludesNotReadyHosts() { + VirtualMachineProfile vmProfile = Mockito.mock(VirtualMachineProfile.class); + Mockito.when(vmProfile.getId()).thenReturn(instanceId); + DataCenter dc = Mockito.mock(DataCenter.class); + Mockito.when(dc.getId()).thenReturn(dataCenterId); + ExcludeList avoids = new ExcludeList(); + + NicVO trunkNic = Mockito.mock(NicVO.class); + Mockito.when(trunkNic.getMultiNetwork()).thenReturn(true); + Mockito.when(nicDao.listByVmId(instanceId)).thenReturn(Arrays.asList(trunkNic)); + + long readyHostId = 10L; + long notReadyHostId = 11L; + long noDetailHostId = 12L; + Mockito.when(hostDao.listEnabledIdsByDataCenterId(dataCenterId)) + .thenReturn(Arrays.asList(readyHostId, notReadyHostId, noDetailHostId)); + + Mockito.when(hostDetailsDao.findHostIdsWithVlanFilteringEnabled()) + .thenReturn(Collections.singleton(readyHostId)); + + _dpm.avoidHostsNotReadyForMultiNetworkNics(vmProfile, dc, avoids); + + Assert.assertEquals(2, avoids.getHostsToAvoid().size()); + Assert.assertTrue(avoids.getHostsToAvoid().contains(notReadyHostId)); + Assert.assertTrue(avoids.getHostsToAvoid().contains(noDetailHostId)); + Assert.assertFalse(avoids.getHostsToAvoid().contains(readyHostId)); + } + @Test public void avoidDisabledPodsTestNoDisabledPod() { DataCenter dc = Mockito.mock(DataCenter.class); @@ -1038,6 +1092,11 @@ public HostDetailsDao hostDetailsDao() { return Mockito.mock(HostDetailsDao.class); } + @Bean + public NicDao nicDao() { + return Mockito.mock(NicDao.class); + } + @Bean public ClusterDetailsDao clusterDetailsDao() { diff --git a/server/src/test/java/com/cloud/network/MockNetworkModelImpl.java b/server/src/test/java/com/cloud/network/MockNetworkModelImpl.java index 3f2f3b6e955e..821935e390af 100644 --- a/server/src/test/java/com/cloud/network/MockNetworkModelImpl.java +++ b/server/src/test/java/com/cloud/network/MockNetworkModelImpl.java @@ -183,6 +183,12 @@ public Nic getNicInNetwork(long vmId, long networkId) { return null; } + @Override + public Pair getNicAndIpInNetwork(long vmId, long networkId) { + // TODO Auto-generated method stub + return null; + } + /* (non-Javadoc) * @see com.cloud.network.NetworkModel#getNicsForTraffic(long, com.cloud.network.Networks.TrafficType) */ diff --git a/server/src/test/java/com/cloud/network/NetworkModelImplTest.java b/server/src/test/java/com/cloud/network/NetworkModelImplTest.java index 7810662c617b..181f0965a3c3 100644 --- a/server/src/test/java/com/cloud/network/NetworkModelImplTest.java +++ b/server/src/test/java/com/cloud/network/NetworkModelImplTest.java @@ -27,6 +27,7 @@ import static org.mockito.Mockito.when; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -71,7 +72,11 @@ import com.cloud.utils.net.Ip; import com.cloud.vm.Nic; import com.cloud.vm.NicProfile; +import com.cloud.vm.NicVO; import com.cloud.vm.VirtualMachine; +import com.cloud.vm.dao.NicDao; +import com.cloud.vm.dao.NicNetworkMapDao; +import com.cloud.vm.dao.NicNetworkMapVO; import org.apache.cloudstack.extension.Extension; import org.apache.cloudstack.extension.ExtensionHelper; import org.apache.cloudstack.framework.extensions.network.NetworkExtensionElement; @@ -452,4 +457,54 @@ public void listSupportedNetworkServiceProvidersExcludesExtensionBackedProviders Mockito.verify(physicalNetworkServiceProviderDao, Mockito.times(1)).listAll(); Mockito.verify(physicalNetworkServiceProviderDao, Mockito.never()).listBy(Mockito.anyLong()); } + + @Test + public void getNicAndIpInNetworkReturnsPrimaryNicWhenItMatches() { + NicDao nicDao = mock(NicDao.class); + networkModel._nicDao = nicDao; + NicVO nic = mock(NicVO.class); + when(nic.getIPv4Address()).thenReturn("10.1.1.5"); + when(nic.getIPv6Address()).thenReturn(null); + when(nicDao.findByNtwkIdAndInstanceId(205L, 5L)).thenReturn(nic); + + Pair result = networkModel.getNicAndIpInNetwork(5L, 205L); + + assertEquals(nic, result.first()); + assertEquals("10.1.1.5", result.second().getIp4Address()); + } + + @Test + public void getNicAndIpInNetworkFallsBackToTrunkAssociationWhenNoPrimaryMatch() { + NicDao nicDao = mock(NicDao.class); + NicNetworkMapDao nicNetworkMapDao = mock(NicNetworkMapDao.class); + networkModel._nicDao = nicDao; + networkModel._nicNetworkMapDao = nicNetworkMapDao; + + when(nicDao.findByNtwkIdAndInstanceId(206L, 5L)).thenReturn(null); + NicVO trunkNic = mock(NicVO.class); + when(trunkNic.getId()).thenReturn(11L); + when(nicDao.listByVmId(5L)).thenReturn(Collections.singletonList(trunkNic)); + NicNetworkMapVO association = mock(NicNetworkMapVO.class); + when(association.getIp4Address()).thenReturn("10.1.1.60"); + when(association.getIp6Address()).thenReturn(null); + when(nicNetworkMapDao.findByNicIdAndNetworkId(11L, 206L)).thenReturn(association); + + Pair result = networkModel.getNicAndIpInNetwork(5L, 206L); + + assertEquals(trunkNic, result.first()); + assertEquals("10.1.1.60", result.second().getIp4Address()); + } + + @Test + public void getNicAndIpInNetworkReturnsNullWhenVmHasNoNicInNetworkAtAll() { + NicDao nicDao = mock(NicDao.class); + NicNetworkMapDao nicNetworkMapDao = mock(NicNetworkMapDao.class); + networkModel._nicDao = nicDao; + networkModel._nicNetworkMapDao = nicNetworkMapDao; + + when(nicDao.findByNtwkIdAndInstanceId(207L, 5L)).thenReturn(null); + when(nicDao.listByVmId(5L)).thenReturn(new ArrayList<>()); + + assertNull(networkModel.getNicAndIpInNetwork(5L, 207L)); + } } diff --git a/server/src/test/java/com/cloud/network/NetworkServiceImplTest.java b/server/src/test/java/com/cloud/network/NetworkServiceImplTest.java index cd7d40d68951..7af232841043 100644 --- a/server/src/test/java/com/cloud/network/NetworkServiceImplTest.java +++ b/server/src/test/java/com/cloud/network/NetworkServiceImplTest.java @@ -34,6 +34,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; +import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.List; @@ -41,6 +42,9 @@ import java.util.UUID; import org.apache.cloudstack.alert.AlertService; +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.admin.network.CreateNetworkCmdByAdmin; import org.apache.cloudstack.api.command.user.address.UpdateQuarantinedIpCmd; import org.apache.cloudstack.api.command.user.network.CreateNetworkCmd; @@ -49,6 +53,7 @@ import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService; import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.network.RoutedIpv4Manager; +import org.apache.cloudstack.storage.template.VnfTemplateManager; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -65,8 +70,13 @@ import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.test.util.ReflectionTestUtils; +import com.cloud.agent.AgentManager; +import com.cloud.agent.api.UpdateNicVlanMembershipAnswer; import com.cloud.agent.api.to.IpAddressTO; import com.cloud.agent.api.to.NicTO; +import com.cloud.hypervisor.Hypervisor; +import com.cloud.hypervisor.HypervisorGuru; +import com.cloud.hypervisor.HypervisorGuruManager; import com.cloud.alert.AlertManager; import com.cloud.bgp.BGPService; import com.cloud.configuration.ConfigurationManager; @@ -75,12 +85,23 @@ import com.cloud.dc.dao.DataCenterDao; import com.cloud.domain.Domain; import com.cloud.domain.DomainVO; +import com.cloud.event.EventTypes; +import com.cloud.event.UsageEventUtils; +import com.cloud.event.UsageEventVO; import com.cloud.domain.dao.DomainDao; import com.cloud.exception.InsufficientAddressCapacityException; import com.cloud.exception.InsufficientCapacityException; import com.cloud.exception.InvalidParameterValueException; +import com.cloud.exception.PermissionDeniedException; import com.cloud.exception.ResourceAllocationException; +import com.cloud.host.DetailVO; +import com.cloud.host.Host; +import com.cloud.host.dao.HostDao; +import com.cloud.host.dao.HostDetailsDao; +import com.cloud.network.dao.FirewallRulesDao; import com.cloud.network.dao.IPAddressDao; +import com.cloud.network.dao.LoadBalancerDao; +import com.cloud.network.rules.dao.PortForwardingRulesDao; import com.cloud.network.dao.IPAddressVO; import com.cloud.network.dao.NetworkDao; import com.cloud.network.dao.NetworkVO; @@ -92,6 +113,10 @@ import com.cloud.network.router.CommandSetupHelper; import com.cloud.network.router.NetworkHelper; import com.cloud.network.vo.PublicIpQuarantineVO; +import com.cloud.vm.UserVmVO; +import com.cloud.vm.dao.NicNetworkMapDao; +import com.cloud.vm.dao.NicNetworkMapVO; +import com.cloud.vm.dao.UserVmDao; import com.cloud.network.vpc.VpcManager; import com.cloud.network.vpc.VpcVO; import com.cloud.network.vpc.dao.VpcDao; @@ -103,6 +128,8 @@ import com.cloud.org.Grouping; import com.cloud.service.ServiceOfferingVO; import com.cloud.service.dao.ServiceOfferingDao; +import com.cloud.storage.VMTemplateVO; +import com.cloud.storage.dao.VMTemplateDao; import com.cloud.user.Account; import com.cloud.user.AccountManager; import com.cloud.user.AccountService; @@ -117,7 +144,9 @@ import com.cloud.utils.db.SearchCriteria; import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.utils.net.Ip; +import com.cloud.utils.net.NetUtils; import com.cloud.vm.DomainRouterVO; +import com.cloud.vm.Nic; import com.cloud.vm.NicVO; import com.cloud.vm.VirtualMachine; import com.cloud.vm.dao.DomainRouterDao; @@ -145,6 +174,12 @@ public class NetworkServiceImplTest { @Mock NetworkModel networkModel; @Mock + AgentManager agentMgr; + @Mock + HypervisorGuruManager hvGuruMgr; + @Mock + HypervisorGuru hypervisorGuru; + @Mock NetworkOfferingServiceMapDao networkOfferingServiceMapDao; @Mock EntityManager entityMgr; @@ -165,8 +200,28 @@ public class NetworkServiceImplTest { @Mock NicDao nicDao; @Mock + UserVmDao userVmDao; + @Mock + HostDao hostDao; + @Mock + HostDetailsDao hostDetailsDao; + @Mock + NicNetworkMapDao nicNetworkMapDao; + @Mock + Ipv6AddressManager ipv6AddressManager; + @Mock IPAddressDao ipAddressDao; @Mock + FirewallRulesDao firewallRulesDao; + @Mock + PortForwardingRulesDao portForwardingRulesDao; + @Mock + LoadBalancerDao loadBalancerDao; + @Mock + VMTemplateDao templateDao; + @Mock + VnfTemplateManager vnfTemplateManagerMock; + @Mock ConfigurationManager configMgr; @Mock ConfigKey publicMtuKey; @@ -267,6 +322,7 @@ public class NetworkServiceImplTest { private VpcVO vpc; private MockedStatic callContextMocked; + private MockedStatic usageEventUtilsMocked; private AutoCloseable closeable; @@ -302,6 +358,7 @@ private void registerCallContext() { public void setup() throws Exception { closeable = MockitoAnnotations.openMocks(this); overrideDefaultConfigValue(NetworkService.AllowUsersToSpecifyVRMtu, "_defaultValue", "true"); + overrideDefaultConfigValue(NetworkOrchestrationService.MultiNetworkNicEnabled, "_defaultValue", "true"); offering = Mockito.mock(NetworkOfferingVO.class); network = Mockito.mock(Network.class); dc = Mockito.mock(DataCenterVO.class); @@ -322,7 +379,20 @@ public void setup() throws Exception { service.networkHelper = networkHelper; service._ipAddrMgr = ipAddressManagerMock; service.nsxProviderDao = nsxProviderDao; + service._userVmDao = userVmDao; + service._hostDao = hostDao; + service._hostDetailsDao = hostDetailsDao; + service._nicNetworkMapDao = nicNetworkMapDao; + service.ipv6AddrMgr = ipv6AddressManager; + service._networkModel = networkModel; + service._entityMgr = entityMgr; + service._networkMgr = _networkMgr; + service._templateDao = templateDao; + service.vnfTemplateManager = vnfTemplateManagerMock; callContextMocked = Mockito.mockStatic(CallContext.class); + // stubbed as a no-op by default: persistAssociations publishes a real UsageEventUtils call on every + // first-time trunk conversion, which this class' tests don't otherwise need to know about + usageEventUtilsMocked = Mockito.mockStatic(UsageEventUtils.class); CallContext callContextMock = Mockito.mock(CallContext.class); callContextMocked.when(CallContext::current).thenReturn(callContextMock); accountMock = Mockito.mock(Account.class); @@ -339,6 +409,7 @@ public void setup() throws Exception { @After public void tearDown() throws Exception { callContextMocked.close(); + usageEventUtilsMocked.close(); closeable.close(); } @@ -1378,4 +1449,884 @@ public void getAndValidateSupportForKeepMacAddressOnPublicNicParameterTestReturn Assert.assertFalse(service.getAndValidateSupportForKeepMacAddressOnPublicNicParameter(false, networkOfferingVO)); } + + private NicVO mockNic(long nicId, long instanceId, long networkId, boolean multiNetwork) { + NicVO nic = Mockito.mock(NicVO.class); + Mockito.when(nic.getId()).thenReturn(nicId); + Mockito.when(nic.getInstanceId()).thenReturn(instanceId); + Mockito.when(nic.getNetworkId()).thenReturn(networkId); + Mockito.when(nic.getVmType()).thenReturn(VirtualMachine.Type.User); + Mockito.when(nic.getUuid()).thenReturn("nic-uuid"); + Mockito.when(nic.getMultiNetwork()).thenReturn(multiNetwork); + return nic; + } + + private NetworkVO mockNetwork(long networkId, Network.GuestType guestType, long physicalNetworkId, long dataCenterId, Long vpcId, + Networks.BroadcastDomainType broadcastType, URI broadcastUri) { + NetworkVO network = Mockito.mock(NetworkVO.class); + Mockito.when(network.getId()).thenReturn(networkId); + Mockito.when(network.getUuid()).thenReturn("network-uuid-" + networkId); + Mockito.when(network.getGuestType()).thenReturn(guestType); + Mockito.when(network.getPhysicalNetworkId()).thenReturn(physicalNetworkId); + Mockito.when(network.getDataCenterId()).thenReturn(dataCenterId); + Mockito.lenient().when(network.getVpcId()).thenReturn(vpcId); + Mockito.when(network.getBroadcastDomainType()).thenReturn(broadcastType); + Mockito.when(network.getBroadcastUri()).thenReturn(broadcastUri); + Mockito.when(network.getState()).thenReturn(Network.State.Implemented); + return network; + } + + private UserVmVO mockVm(VirtualMachine.State state, Long hostId) { + UserVmVO vm = Mockito.mock(UserVmVO.class); + Mockito.when(vm.getState()).thenReturn(state); + Mockito.when(vm.getHostId()).thenReturn(hostId); + return vm; + } + + private AssociateNetworkToNicCmd mockAssociateCmd(long nicId, List networkIds) { + AssociateNetworkToNicCmd cmd = Mockito.mock(AssociateNetworkToNicCmd.class); + Mockito.when(cmd.getNicId()).thenReturn(nicId); + Mockito.when(cmd.getNetworkIds()).thenReturn(networkIds); + Mockito.when(cmd.getIpAddressesMap()).thenReturn(new HashMap<>()); + return cmd; + } + + @Test(expected = InvalidParameterValueException.class) + public void associateNetworkToNicFailsWhenNicNotFound() throws Exception { + Mockito.when(nicDao.findById(11L)).thenReturn(null); + + service.associateNetworkToNic(mockAssociateCmd(11L, Arrays.asList(206L))); + } + + @Test(expected = InvalidParameterValueException.class) + public void associateNetworkToNicFailsWhenNicIsNotOnAUserVm() throws Exception { + NicVO nic = mockNic(11L, 5L, 205L, false); + Mockito.when(nic.getVmType()).thenReturn(VirtualMachine.Type.DomainRouter); + Mockito.when(nicDao.findById(11L)).thenReturn(nic); + + service.associateNetworkToNic(mockAssociateCmd(11L, Arrays.asList(206L))); + } + + @Test + public void associateNetworkToNicFailsWhenFeatureDisabledInZone() throws Exception { + NicVO nic = mockNic(11L, 5L, 205L, false); + Mockito.when(nicDao.findById(11L)).thenReturn(nic); + UserVmVO vm = mockVm(VirtualMachine.State.Stopped, null); + Mockito.when(userVmDao.findById(5L)).thenReturn(vm); + + overrideDefaultConfigValue(NetworkOrchestrationService.MultiNetworkNicEnabled, "_defaultValue", "false"); + try { + service.associateNetworkToNic(mockAssociateCmd(11L, Arrays.asList(206L))); + Assert.fail("expected InvalidParameterValueException"); + } catch (InvalidParameterValueException e) { + Assert.assertTrue(e.getMessage().contains("not enabled")); + } finally { + overrideDefaultConfigValue(NetworkOrchestrationService.MultiNetworkNicEnabled, "_defaultValue", "true"); + } + } + + @Test(expected = InvalidParameterValueException.class) + public void associateNetworkToNicFailsWhenOwningVmNotFound() throws Exception { + NicVO nic = mockNic(11L, 5L, 205L, false); + Mockito.when(nicDao.findById(11L)).thenReturn(nic); + Mockito.when(userVmDao.findById(5L)).thenReturn(null); + + service.associateNetworkToNic(mockAssociateCmd(11L, Arrays.asList(206L))); + } + + @Test(expected = InvalidParameterValueException.class) + public void associateNetworkToNicFailsWhenRunningVmHostIsNotVlanFilteringReady() throws Exception { + NicVO nic = mockNic(11L, 5L, 205L, false); + Mockito.when(nicDao.findById(11L)).thenReturn(nic); + UserVmVO vm = mockVm(VirtualMachine.State.Running, 1L); + Mockito.when(userVmDao.findById(5L)).thenReturn(vm); + Mockito.when(hostDetailsDao.findDetail(1L, Host.HOST_VLAN_FILTERING_ENABLED)).thenReturn(null); + + service.associateNetworkToNic(mockAssociateCmd(11L, Arrays.asList(206L))); + } + + @Test + public void associateNetworkToNicUpdatesLiveVlanMembershipOnARunningVmWhenHostIsReady() throws Exception { + NicVO nic = mockNic(11L, 5L, 205L, false); + Mockito.when(nicDao.findById(11L)).thenReturn(nic); + UserVmVO vm = mockVm(VirtualMachine.State.Running, 1L); + Mockito.when(vm.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.KVM); + Mockito.when(vm.getInstanceName()).thenReturn("i-2-11-VM"); + Mockito.when(userVmDao.findById(5L)).thenReturn(vm); + DetailVO readyDetail = Mockito.mock(DetailVO.class); + Mockito.when(readyDetail.getValue()).thenReturn("true"); + Mockito.when(hostDetailsDao.findDetail(1L, Host.HOST_VLAN_FILTERING_ENABLED)).thenReturn(readyDetail); + NetworkVO primaryNetwork = mockNetwork(205L, Network.GuestType.Isolated, 1L, 1L, null, Networks.BroadcastDomainType.Vlan, vlanUri(1174)); + Mockito.when(networkDao.findById(205L)).thenReturn(primaryNetwork); + Mockito.when(nicNetworkMapDao.listByNicId(11L)).thenReturn(new ArrayList<>()); + NetworkVO requestedNetwork = mockNetwork(206L, Network.GuestType.Isolated, 1L, 1L, null, Networks.BroadcastDomainType.Vlan, vlanUri(1180)); + Mockito.when(networkDao.findById(206L)).thenReturn(requestedNetwork); + Mockito.when(ipAddressManagerMock.allocateGuestIP(requestedNetwork, null)).thenReturn("10.1.1.50"); + Mockito.when(networkModel.getNicProfile(Mockito.eq(vm), Mockito.eq(nic), Mockito.any())).thenReturn(null); + Mockito.when(hvGuruMgr.getGuru(Hypervisor.HypervisorType.KVM)).thenReturn(hypervisorGuru); + Mockito.when(hypervisorGuru.toNicTO(null)).thenReturn(Mockito.mock(NicTO.class)); + Mockito.when(agentMgr.easySend(Mockito.eq(1L), Mockito.any())).thenReturn(new UpdateNicVlanMembershipAnswer(null, true, "success")); + DomainRouterVO router = Mockito.mock(DomainRouterVO.class); + Mockito.when(router.getState()).thenReturn(VirtualMachine.State.Running); + Mockito.when(routerDao.findByNetwork(205L)).thenReturn(Arrays.asList(router)); + + Nic result = service.associateNetworkToNic(mockAssociateCmd(11L, Arrays.asList(206L))); + + Assert.assertNotNull(result); + Mockito.verify(agentMgr).easySend(Mockito.eq(1L), Mockito.any()); + Mockito.verify(commandSetupHelper).createVmDataCommand(Mockito.eq(router), Mockito.eq(vm), Mockito.eq(nic), Mockito.any(), Mockito.any()); + Mockito.verify(networkHelper).sendCommandsToRouter(Mockito.eq(router), Mockito.any()); + } + + @Test + public void associateNetworkToNicPushesDhcpEntryForNewlyAssociatedNetworkOnARunningVm() throws Exception { + NicVO nic = mockNic(11L, 5L, 205L, false); + Mockito.when(nicDao.findById(11L)).thenReturn(nic); + UserVmVO vm = mockVm(VirtualMachine.State.Running, 1L); + Mockito.when(vm.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.KVM); + Mockito.when(vm.getInstanceName()).thenReturn("i-2-11-VM"); + Mockito.when(userVmDao.findById(5L)).thenReturn(vm); + DetailVO readyDetail = Mockito.mock(DetailVO.class); + Mockito.when(readyDetail.getValue()).thenReturn("true"); + Mockito.when(hostDetailsDao.findDetail(1L, Host.HOST_VLAN_FILTERING_ENABLED)).thenReturn(readyDetail); + NetworkVO primaryNetwork = mockNetwork(205L, Network.GuestType.Isolated, 1L, 1L, null, Networks.BroadcastDomainType.Vlan, vlanUri(1174)); + Mockito.when(networkDao.findById(205L)).thenReturn(primaryNetwork); + Mockito.when(nicNetworkMapDao.listByNicId(11L)).thenReturn(new ArrayList<>()); + NetworkVO requestedNetwork = mockNetwork(206L, Network.GuestType.Isolated, 1L, 1L, null, Networks.BroadcastDomainType.Vlan, vlanUri(1180)); + Mockito.when(networkDao.findById(206L)).thenReturn(requestedNetwork); + Mockito.when(ipAddressManagerMock.allocateGuestIP(requestedNetwork, null)).thenReturn("10.1.1.50"); + Mockito.when(nicNetworkMapDao.findByNicIdAndNetworkId(11L, 206L)).thenReturn(new NicNetworkMapVO(11L, 206L, "10.1.1.50", null)); + Mockito.when(networkModel.getNicProfile(Mockito.eq(vm), Mockito.eq(nic), Mockito.any())).thenReturn(null); + Mockito.when(hvGuruMgr.getGuru(Hypervisor.HypervisorType.KVM)).thenReturn(hypervisorGuru); + Mockito.when(hypervisorGuru.toNicTO(null)).thenReturn(Mockito.mock(NicTO.class)); + Mockito.when(agentMgr.easySend(Mockito.eq(1L), Mockito.any())).thenReturn(new UpdateNicVlanMembershipAnswer(null, true, "success")); + Mockito.when(routerDao.findByNetwork(205L)).thenReturn(new ArrayList<>()); + DomainRouterVO associatedNetworkRouter = Mockito.mock(DomainRouterVO.class); + Mockito.when(associatedNetworkRouter.getState()).thenReturn(VirtualMachine.State.Running); + Mockito.when(routerDao.findByNetwork(206L)).thenReturn(Arrays.asList(associatedNetworkRouter)); + + service.associateNetworkToNic(mockAssociateCmd(11L, Arrays.asList(206L))); + + Mockito.verify(commandSetupHelper).createDhcpEntryCommand(Mockito.eq(associatedNetworkRouter), Mockito.eq(vm), Mockito.any(), Mockito.eq("10.1.1.50"), + Mockito.any(), Mockito.any(), Mockito.any(), Mockito.eq(206L), Mockito.eq(false), Mockito.eq(false), Mockito.any()); + Mockito.verify(networkHelper).sendCommandsToRouter(Mockito.eq(associatedNetworkRouter), Mockito.any()); + } + + @Test + public void associateNetworkToNicDoesNotPushDhcpEntryOnAStoppedVm() throws Exception { + NicVO nic = mockNic(11L, 5L, 205L, false); + Mockito.when(nicDao.findById(11L)).thenReturn(nic); + UserVmVO vm = mockVm(VirtualMachine.State.Stopped, null); + Mockito.when(userVmDao.findById(5L)).thenReturn(vm); + NetworkVO primaryNetwork = mockNetwork(205L, Network.GuestType.Isolated, 1L, 1L, null, Networks.BroadcastDomainType.Vlan, vlanUri(1174)); + Mockito.when(networkDao.findById(205L)).thenReturn(primaryNetwork); + Mockito.when(nicNetworkMapDao.listByNicId(11L)).thenReturn(new ArrayList<>()); + NetworkVO requestedNetwork = mockNetwork(206L, Network.GuestType.Isolated, 1L, 1L, null, Networks.BroadcastDomainType.Vlan, vlanUri(1180)); + Mockito.when(networkDao.findById(206L)).thenReturn(requestedNetwork); + Mockito.when(ipAddressManagerMock.allocateGuestIP(requestedNetwork, null)).thenReturn("10.1.1.50"); + + service.associateNetworkToNic(mockAssociateCmd(11L, Arrays.asList(206L))); + + Mockito.verify(commandSetupHelper, Mockito.never()).createDhcpEntryCommand(Mockito.any(), Mockito.any(), Mockito.anyString(), Mockito.anyString(), + Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyLong(), Mockito.anyBoolean(), Mockito.anyBoolean(), Mockito.any()); + } + + @Test + public void associateNetworkToNicDoesNotRefreshMetadataOnRoutersThatAreNotRunning() throws Exception { + NicVO nic = mockNic(11L, 5L, 205L, false); + Mockito.when(nicDao.findById(11L)).thenReturn(nic); + UserVmVO vm = mockVm(VirtualMachine.State.Running, 1L); + Mockito.when(vm.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.KVM); + Mockito.when(vm.getInstanceName()).thenReturn("i-2-11-VM"); + Mockito.when(userVmDao.findById(5L)).thenReturn(vm); + DetailVO readyDetail = Mockito.mock(DetailVO.class); + Mockito.when(readyDetail.getValue()).thenReturn("true"); + Mockito.when(hostDetailsDao.findDetail(1L, Host.HOST_VLAN_FILTERING_ENABLED)).thenReturn(readyDetail); + NetworkVO primaryNetwork = mockNetwork(205L, Network.GuestType.Isolated, 1L, 1L, null, Networks.BroadcastDomainType.Vlan, vlanUri(1174)); + Mockito.when(networkDao.findById(205L)).thenReturn(primaryNetwork); + Mockito.when(nicNetworkMapDao.listByNicId(11L)).thenReturn(new ArrayList<>()); + NetworkVO requestedNetwork = mockNetwork(206L, Network.GuestType.Isolated, 1L, 1L, null, Networks.BroadcastDomainType.Vlan, vlanUri(1180)); + Mockito.when(networkDao.findById(206L)).thenReturn(requestedNetwork); + Mockito.when(ipAddressManagerMock.allocateGuestIP(requestedNetwork, null)).thenReturn("10.1.1.50"); + Mockito.when(networkModel.getNicProfile(Mockito.eq(vm), Mockito.eq(nic), Mockito.any())).thenReturn(null); + Mockito.when(hvGuruMgr.getGuru(Hypervisor.HypervisorType.KVM)).thenReturn(hypervisorGuru); + Mockito.when(hypervisorGuru.toNicTO(null)).thenReturn(Mockito.mock(NicTO.class)); + Mockito.when(agentMgr.easySend(Mockito.eq(1L), Mockito.any())).thenReturn(new UpdateNicVlanMembershipAnswer(null, true, "success")); + DomainRouterVO stoppedRouter = Mockito.mock(DomainRouterVO.class); + Mockito.when(stoppedRouter.getState()).thenReturn(VirtualMachine.State.Stopped); + Mockito.when(routerDao.findByNetwork(205L)).thenReturn(Arrays.asList(stoppedRouter)); + + service.associateNetworkToNic(mockAssociateCmd(11L, Arrays.asList(206L))); + + Mockito.verify(commandSetupHelper, Mockito.never()).createVmDataCommand(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()); + } + + @Test(expected = CloudRuntimeException.class) + public void associateNetworkToNicFailsWhenLiveUpdateCommandFails() throws Exception { + NicVO nic = mockNic(11L, 5L, 205L, false); + Mockito.when(nicDao.findById(11L)).thenReturn(nic); + UserVmVO vm = mockVm(VirtualMachine.State.Running, 1L); + Mockito.when(vm.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.KVM); + Mockito.when(vm.getInstanceName()).thenReturn("i-2-11-VM"); + Mockito.when(userVmDao.findById(5L)).thenReturn(vm); + DetailVO readyDetail = Mockito.mock(DetailVO.class); + Mockito.when(readyDetail.getValue()).thenReturn("true"); + Mockito.when(hostDetailsDao.findDetail(1L, Host.HOST_VLAN_FILTERING_ENABLED)).thenReturn(readyDetail); + NetworkVO primaryNetwork = mockNetwork(205L, Network.GuestType.Isolated, 1L, 1L, null, Networks.BroadcastDomainType.Vlan, vlanUri(1174)); + Mockito.when(networkDao.findById(205L)).thenReturn(primaryNetwork); + Mockito.when(nicNetworkMapDao.listByNicId(11L)).thenReturn(new ArrayList<>()); + NetworkVO requestedNetwork = mockNetwork(206L, Network.GuestType.Isolated, 1L, 1L, null, Networks.BroadcastDomainType.Vlan, vlanUri(1180)); + Mockito.when(networkDao.findById(206L)).thenReturn(requestedNetwork); + Mockito.when(ipAddressManagerMock.allocateGuestIP(requestedNetwork, null)).thenReturn("10.1.1.50"); + Mockito.when(networkModel.getNicProfile(Mockito.eq(vm), Mockito.eq(nic), Mockito.any())).thenReturn(null); + Mockito.when(hvGuruMgr.getGuru(Hypervisor.HypervisorType.KVM)).thenReturn(hypervisorGuru); + Mockito.when(hypervisorGuru.toNicTO(null)).thenReturn(Mockito.mock(NicTO.class)); + Mockito.when(agentMgr.easySend(Mockito.eq(1L), Mockito.any())).thenReturn(new UpdateNicVlanMembershipAnswer(null, false, "boom")); + + service.associateNetworkToNic(mockAssociateCmd(11L, Arrays.asList(206L))); + } + + @Test(expected = InvalidParameterValueException.class) + public void associateNetworkToNicFailsWhenAlreadyAssociatedWithPrimaryNetwork() throws Exception { + NicVO nic = mockNic(11L, 5L, 205L, false); + Mockito.when(nicDao.findById(11L)).thenReturn(nic); + UserVmVO vm = mockVm(VirtualMachine.State.Stopped, null); + Mockito.when(userVmDao.findById(5L)).thenReturn(vm); + NetworkVO primaryNetwork = mockNetwork(205L, Network.GuestType.Isolated, 1L, 1L, null, Networks.BroadcastDomainType.Vlan, vlanUri(1174)); + Mockito.when(networkDao.findById(205L)).thenReturn(primaryNetwork); + Mockito.when(nicNetworkMapDao.listByNicId(11L)).thenReturn(new ArrayList<>()); + + service.associateNetworkToNic(mockAssociateCmd(11L, Arrays.asList(205L))); + } + + @Test(expected = InvalidParameterValueException.class) + public void associateNetworkToNicFailsWhenRequestedNetworkIsADifferentGuestType() throws Exception { + NicVO nic = mockNic(11L, 5L, 205L, false); + Mockito.when(nicDao.findById(11L)).thenReturn(nic); + UserVmVO vm = mockVm(VirtualMachine.State.Stopped, null); + Mockito.when(userVmDao.findById(5L)).thenReturn(vm); + NetworkVO primaryNetwork = mockNetwork(205L, Network.GuestType.Isolated, 1L, 1L, null, Networks.BroadcastDomainType.Vlan, vlanUri(1174)); + Mockito.when(networkDao.findById(205L)).thenReturn(primaryNetwork); + Mockito.when(nicNetworkMapDao.listByNicId(11L)).thenReturn(new ArrayList<>()); + NetworkVO sharedNetwork = mockNetwork(206L, Network.GuestType.Shared, 1L, 1L, null, Networks.BroadcastDomainType.Vlan, vlanUri(1180)); + Mockito.when(networkDao.findById(206L)).thenReturn(sharedNetwork); + + service.associateNetworkToNic(mockAssociateCmd(11L, Arrays.asList(206L))); + } + + @Test(expected = InvalidParameterValueException.class) + public void associateNetworkToNicFailsWhenRequestedNetworkCidrOverlapsPrimaryNetworkCidr() throws Exception { + NicVO nic = mockNic(11L, 5L, 205L, false); + Mockito.when(nicDao.findById(11L)).thenReturn(nic); + UserVmVO vm = mockVm(VirtualMachine.State.Stopped, null); + Mockito.when(userVmDao.findById(5L)).thenReturn(vm); + NetworkVO primaryNetwork = mockNetwork(205L, Network.GuestType.Isolated, 1L, 1L, null, Networks.BroadcastDomainType.Vlan, vlanUri(1174)); + Mockito.when(primaryNetwork.getCidr()).thenReturn("10.1.1.0/24"); + Mockito.when(networkDao.findById(205L)).thenReturn(primaryNetwork); + Mockito.when(nicNetworkMapDao.listByNicId(11L)).thenReturn(new ArrayList<>()); + NetworkVO requestedNetwork = mockNetwork(206L, Network.GuestType.Isolated, 1L, 1L, null, Networks.BroadcastDomainType.Vlan, vlanUri(1180)); + Mockito.when(requestedNetwork.getCidr()).thenReturn("10.1.1.0/24"); + Mockito.when(networkDao.findById(206L)).thenReturn(requestedNetwork); + + service.associateNetworkToNic(mockAssociateCmd(11L, Arrays.asList(206L))); + } + + @Test + public void associateNetworkToNicSucceedsWhenRequestedNetworkCidrDoesNotOverlapPrimaryNetworkCidr() throws Exception { + NicVO nic = mockNic(11L, 5L, 205L, false); + Mockito.when(nicDao.findById(11L)).thenReturn(nic); + UserVmVO vm = mockVm(VirtualMachine.State.Stopped, null); + Mockito.when(userVmDao.findById(5L)).thenReturn(vm); + NetworkVO primaryNetwork = mockNetwork(205L, Network.GuestType.Isolated, 1L, 1L, null, Networks.BroadcastDomainType.Vlan, vlanUri(1174)); + Mockito.when(primaryNetwork.getCidr()).thenReturn("10.1.1.0/24"); + Mockito.when(networkDao.findById(205L)).thenReturn(primaryNetwork); + Mockito.when(nicNetworkMapDao.listByNicId(11L)).thenReturn(new ArrayList<>()); + NetworkVO requestedNetwork = mockNetwork(206L, Network.GuestType.Isolated, 1L, 1L, null, Networks.BroadcastDomainType.Vlan, vlanUri(1180)); + Mockito.when(requestedNetwork.getCidr()).thenReturn("10.1.2.0/24"); + Mockito.when(networkDao.findById(206L)).thenReturn(requestedNetwork); + Mockito.when(ipAddressManagerMock.allocateGuestIP(requestedNetwork, null)).thenReturn("10.1.2.50"); + + Nic result = service.associateNetworkToNic(mockAssociateCmd(11L, Arrays.asList(206L))); + + Assert.assertNotNull(result); + } + + @Test + public void associateNetworkToNicChecksCallerAccessToRequestedNetwork() throws Exception { + NicVO nic = mockNic(11L, 5L, 205L, false); + Mockito.when(nicDao.findById(11L)).thenReturn(nic); + UserVmVO vm = mockVm(VirtualMachine.State.Stopped, null); + Mockito.when(userVmDao.findById(5L)).thenReturn(vm); + NetworkVO primaryNetwork = mockNetwork(205L, Network.GuestType.Isolated, 1L, 1L, null, Networks.BroadcastDomainType.Vlan, vlanUri(1174)); + Mockito.when(primaryNetwork.getCidr()).thenReturn("10.1.1.0/24"); + Mockito.when(networkDao.findById(205L)).thenReturn(primaryNetwork); + Mockito.when(nicNetworkMapDao.listByNicId(11L)).thenReturn(new ArrayList<>()); + NetworkVO requestedNetwork = mockNetwork(206L, Network.GuestType.Isolated, 1L, 1L, null, Networks.BroadcastDomainType.Vlan, vlanUri(1180)); + Mockito.when(requestedNetwork.getCidr()).thenReturn("10.1.2.0/24"); + Mockito.when(networkDao.findById(206L)).thenReturn(requestedNetwork); + Mockito.when(ipAddressManagerMock.allocateGuestIP(requestedNetwork, null)).thenReturn("10.1.2.50"); + + service.associateNetworkToNic(mockAssociateCmd(11L, Arrays.asList(206L))); + + Mockito.verify(accountManager).checkAccess(accountMock, null, true, requestedNetwork); + } + + @Test(expected = PermissionDeniedException.class) + public void associateNetworkToNicFailsWhenCallerLacksAccessToRequestedNetwork() throws Exception { + NicVO nic = mockNic(11L, 5L, 205L, false); + Mockito.when(nicDao.findById(11L)).thenReturn(nic); + UserVmVO vm = mockVm(VirtualMachine.State.Stopped, null); + Mockito.when(userVmDao.findById(5L)).thenReturn(vm); + NetworkVO primaryNetwork = mockNetwork(205L, Network.GuestType.Isolated, 1L, 1L, null, Networks.BroadcastDomainType.Vlan, vlanUri(1174)); + Mockito.when(networkDao.findById(205L)).thenReturn(primaryNetwork); + Mockito.when(nicNetworkMapDao.listByNicId(11L)).thenReturn(new ArrayList<>()); + NetworkVO requestedNetwork = mockNetwork(206L, Network.GuestType.Isolated, 1L, 1L, null, Networks.BroadcastDomainType.Vlan, vlanUri(1180)); + Mockito.when(networkDao.findById(206L)).thenReturn(requestedNetwork); + Mockito.doThrow(new PermissionDeniedException("no access to network 206")) + .when(accountManager).checkAccess(accountMock, null, true, requestedNetwork); + + service.associateNetworkToNic(mockAssociateCmd(11L, Arrays.asList(206L))); + } + + @Test(expected = InvalidParameterValueException.class) + public void associateNetworkToNicFailsWhenRequestedNetworkIsOnADifferentPhysicalNetwork() throws Exception { + NicVO nic = mockNic(11L, 5L, 205L, false); + Mockito.when(nicDao.findById(11L)).thenReturn(nic); + UserVmVO vm = mockVm(VirtualMachine.State.Stopped, null); + Mockito.when(userVmDao.findById(5L)).thenReturn(vm); + NetworkVO primaryNetwork = mockNetwork(205L, Network.GuestType.Isolated, 1L, 1L, null, Networks.BroadcastDomainType.Vlan, vlanUri(1174)); + Mockito.when(networkDao.findById(205L)).thenReturn(primaryNetwork); + Mockito.when(nicNetworkMapDao.listByNicId(11L)).thenReturn(new ArrayList<>()); + NetworkVO otherPhysicalNetwork = mockNetwork(206L, Network.GuestType.Isolated, 2L, 1L, null, Networks.BroadcastDomainType.Vlan, vlanUri(1180)); + Mockito.when(networkDao.findById(206L)).thenReturn(otherPhysicalNetwork); + + service.associateNetworkToNic(mockAssociateCmd(11L, Arrays.asList(206L))); + } + + @Test(expected = InvalidParameterValueException.class) + public void associateNetworkToNicFailsWhenRequestedNetworkIsInADifferentZone() throws Exception { + NicVO nic = mockNic(11L, 5L, 205L, false); + Mockito.when(nicDao.findById(11L)).thenReturn(nic); + UserVmVO vm = mockVm(VirtualMachine.State.Stopped, null); + Mockito.when(userVmDao.findById(5L)).thenReturn(vm); + NetworkVO primaryNetwork = mockNetwork(205L, Network.GuestType.Isolated, 1L, 1L, null, Networks.BroadcastDomainType.Vlan, vlanUri(1174)); + Mockito.when(networkDao.findById(205L)).thenReturn(primaryNetwork); + Mockito.when(nicNetworkMapDao.listByNicId(11L)).thenReturn(new ArrayList<>()); + NetworkVO otherZoneNetwork = mockNetwork(206L, Network.GuestType.Isolated, 1L, 2L, null, Networks.BroadcastDomainType.Vlan, vlanUri(1180)); + Mockito.when(networkDao.findById(206L)).thenReturn(otherZoneNetwork); + + service.associateNetworkToNic(mockAssociateCmd(11L, Arrays.asList(206L))); + } + + @Test + public void associateNetworkToNicAllowsRequestedIsolatedNetworkInADifferentVpc() throws Exception { + // same-VPC restriction was dropped: caller access (checkAccess) is the real authorization boundary here, + // identical to the one ordinary addNicToVirtualMachine already relies on, so a tier of an unrelated VPC + // (or a non-VPC network) is just as valid an association as a tier of the primary network's own VPC. + NicVO nic = mockNic(11L, 5L, 205L, false); + Mockito.when(nicDao.findById(11L)).thenReturn(nic); + UserVmVO vm = mockVm(VirtualMachine.State.Stopped, null); + Mockito.when(userVmDao.findById(5L)).thenReturn(vm); + NetworkVO primaryNetwork = mockNetwork(205L, Network.GuestType.Isolated, 1L, 1L, 900L, Networks.BroadcastDomainType.Vlan, vlanUri(1174)); + Mockito.when(networkDao.findById(205L)).thenReturn(primaryNetwork); + Mockito.when(nicNetworkMapDao.listByNicId(11L)).thenReturn(new ArrayList<>()); + NetworkVO otherVpcNetwork = mockNetwork(206L, Network.GuestType.Isolated, 1L, 1L, 901L, Networks.BroadcastDomainType.Vlan, vlanUri(1180)); + Mockito.when(networkDao.findById(206L)).thenReturn(otherVpcNetwork); + Mockito.when(ipAddressManagerMock.allocateGuestIP(otherVpcNetwork, null)).thenReturn("10.1.1.51"); + + Nic result = service.associateNetworkToNic(mockAssociateCmd(11L, Arrays.asList(206L))); + + Assert.assertNotNull(result); + Mockito.verify(nicNetworkMapDao).persist(Mockito.argThat(map -> + map.getNicId() == 11L && map.getNetworkId() == 206L && "10.1.1.51".equals(map.getIp4Address()))); + } + + @Test(expected = InvalidParameterValueException.class) + public void associateNetworkToNicFailsWhenRequestedNetworkIsSecurityGroupEnabled() throws Exception { + NicVO nic = mockNic(11L, 5L, 205L, false); + Mockito.when(nicDao.findById(11L)).thenReturn(nic); + UserVmVO vm = mockVm(VirtualMachine.State.Stopped, null); + Mockito.when(userVmDao.findById(5L)).thenReturn(vm); + NetworkVO primaryNetwork = mockNetwork(205L, Network.GuestType.Isolated, 1L, 1L, null, Networks.BroadcastDomainType.Vlan, vlanUri(1174)); + Mockito.when(networkDao.findById(205L)).thenReturn(primaryNetwork); + Mockito.when(nicNetworkMapDao.listByNicId(11L)).thenReturn(new ArrayList<>()); + NetworkVO sgNetwork = mockNetwork(206L, Network.GuestType.Isolated, 1L, 1L, null, Networks.BroadcastDomainType.Vlan, vlanUri(1180)); + Mockito.when(networkDao.findById(206L)).thenReturn(sgNetwork); + Mockito.when(networkModel.isSecurityGroupSupportedInNetwork(sgNetwork)).thenReturn(true); + + service.associateNetworkToNic(mockAssociateCmd(11L, Arrays.asList(206L))); + } + + @Test(expected = InvalidParameterValueException.class) + public void associateNetworkToNicFailsWhenRequestedNetworkSharesVlanWithAnExistingAssociation() throws Exception { + NicVO nic = mockNic(11L, 5L, 205L, false); + Mockito.when(nicDao.findById(11L)).thenReturn(nic); + UserVmVO vm = mockVm(VirtualMachine.State.Stopped, null); + Mockito.when(userVmDao.findById(5L)).thenReturn(vm); + NetworkVO primaryNetwork = mockNetwork(205L, Network.GuestType.Isolated, 1L, 1L, null, Networks.BroadcastDomainType.Vlan, vlanUri(1174)); + Mockito.when(networkDao.findById(205L)).thenReturn(primaryNetwork); + Mockito.when(nicNetworkMapDao.listByNicId(11L)).thenReturn(new ArrayList<>()); + NetworkVO sameVlanNetwork = mockNetwork(206L, Network.GuestType.Isolated, 1L, 1L, null, Networks.BroadcastDomainType.Vlan, vlanUri(1174)); + Mockito.when(networkDao.findById(206L)).thenReturn(sameVlanNetwork); + + service.associateNetworkToNic(mockAssociateCmd(11L, Arrays.asList(206L))); + } + + @Test + public void associateNetworkToNicPersistsAssociationAndSetsMultiNetworkOnAStoppedVm() throws Exception { + NicVO nic = mockNic(11L, 5L, 205L, false); + Mockito.when(nicDao.findById(11L)).thenReturn(nic); + UserVmVO vm = mockVm(VirtualMachine.State.Stopped, null); + Mockito.when(userVmDao.findById(5L)).thenReturn(vm); + NetworkVO primaryNetwork = mockNetwork(205L, Network.GuestType.Isolated, 1L, 1L, null, Networks.BroadcastDomainType.Vlan, vlanUri(1174)); + Mockito.when(networkDao.findById(205L)).thenReturn(primaryNetwork); + Mockito.when(nicNetworkMapDao.listByNicId(11L)).thenReturn(new ArrayList<>()); + NetworkVO requestedNetwork = mockNetwork(206L, Network.GuestType.Isolated, 1L, 1L, null, Networks.BroadcastDomainType.Vlan, vlanUri(1180)); + Mockito.when(networkDao.findById(206L)).thenReturn(requestedNetwork); + Mockito.when(ipAddressManagerMock.allocateGuestIP(requestedNetwork, null)).thenReturn("10.1.1.50"); + + Nic result = service.associateNetworkToNic(mockAssociateCmd(11L, Arrays.asList(206L))); + + Assert.assertNotNull(result); + Mockito.verify(nicNetworkMapDao).persist(Mockito.argThat(map -> + map.getNicId() == 11L && map.getNetworkId() == 206L && "10.1.1.50".equals(map.getIp4Address()))); + Mockito.verify(nic).setMultiNetwork(true); + Mockito.verify(nicDao).update(Mockito.eq(11L), Mockito.eq(nic)); + } + + @Test + public void associateNetworksToNicPersistsAssociationWithNoExplicitIpAddresses() throws Exception { + // the deploy-time path (persistAdditionalNicNetworkAssociations -> associateNetworksToNic) calls this with + // no ipAddresses map at all - must not NPE when that's passed through as a literal null + NicVO nic = mockNic(11L, 5L, 205L, false); + Mockito.when(nicDao.findById(11L)).thenReturn(nic); + UserVmVO vm = mockVm(VirtualMachine.State.Stopped, null); + Mockito.when(userVmDao.findById(5L)).thenReturn(vm); + NetworkVO primaryNetwork = mockNetwork(205L, Network.GuestType.Isolated, 1L, 1L, null, Networks.BroadcastDomainType.Vlan, vlanUri(1174)); + Mockito.when(networkDao.findById(205L)).thenReturn(primaryNetwork); + Mockito.when(nicNetworkMapDao.listByNicId(11L)).thenReturn(new ArrayList<>()); + NetworkVO requestedNetwork = mockNetwork(206L, Network.GuestType.Isolated, 1L, 1L, null, Networks.BroadcastDomainType.Vlan, vlanUri(1180)); + Mockito.when(networkDao.findById(206L)).thenReturn(requestedNetwork); + Mockito.when(ipAddressManagerMock.allocateGuestIP(requestedNetwork, null)).thenReturn("10.1.1.60"); + + service.associateNetworksToNic(nic, Arrays.asList(206L), null); + + Mockito.verify(nicNetworkMapDao).persist(Mockito.argThat(map -> + map.getNicId() == 11L && map.getNetworkId() == 206L && "10.1.1.60".equals(map.getIp4Address()))); + Mockito.verify(nic).setMultiNetwork(true); + } + + @Test + public void associateNetworksToNicUsesRequestedIpForAssociatedNetwork() throws Exception { + // deploy-time path (nicnetworkslist[N].ip4addresses) - a requested ip for an associated network must + // reach allocateGuestIP as the requested value, not be silently ignored/auto-allocated + NicVO nic = mockNic(11L, 5L, 205L, false); + Mockito.when(nicDao.findById(11L)).thenReturn(nic); + UserVmVO vm = mockVm(VirtualMachine.State.Stopped, null); + Mockito.when(userVmDao.findById(5L)).thenReturn(vm); + NetworkVO primaryNetwork = mockNetwork(205L, Network.GuestType.Isolated, 1L, 1L, null, Networks.BroadcastDomainType.Vlan, vlanUri(1174)); + Mockito.when(networkDao.findById(205L)).thenReturn(primaryNetwork); + Mockito.when(nicNetworkMapDao.listByNicId(11L)).thenReturn(new ArrayList<>()); + NetworkVO requestedNetwork = mockNetwork(206L, Network.GuestType.Isolated, 1L, 1L, null, Networks.BroadcastDomainType.Vlan, vlanUri(1180)); + Mockito.when(networkDao.findById(206L)).thenReturn(requestedNetwork); + Mockito.when(ipAddressManagerMock.allocateGuestIP(requestedNetwork, "10.1.1.99")).thenReturn("10.1.1.99"); + + service.associateNetworksToNic(nic, Arrays.asList(206L), Collections.singletonMap(206L, new Network.IpAddresses("10.1.1.99", null))); + + Mockito.verify(ipAddressManagerMock).allocateGuestIP(requestedNetwork, "10.1.1.99"); + Mockito.verify(nicNetworkMapDao).persist(Mockito.argThat(map -> + map.getNicId() == 11L && map.getNetworkId() == 206L && "10.1.1.99".equals(map.getIp4Address()))); + } + + @Test(expected = InvalidParameterValueException.class) + public void associateNetworksToNicRejectsVnfManagementNic() throws Exception { + // the live associate-network-to-nic path must reject trunking a VNF template's management nic, same as + // the deploy-time nicnetworkslist path already does via validateVnfApplianceTrunkNics + NicVO nic = mockNic(11L, 5L, 205L, false); + Mockito.when(nicDao.findById(11L)).thenReturn(nic); + UserVmVO vm = mockVm(VirtualMachine.State.Stopped, null); + Mockito.when(vm.getTemplateId()).thenReturn(99L); + Mockito.when(userVmDao.findById(5L)).thenReturn(vm); + NetworkVO primaryNetwork = mockNetwork(205L, Network.GuestType.Isolated, 1L, 1L, null, Networks.BroadcastDomainType.Vlan, vlanUri(1174)); + Mockito.when(networkDao.findById(205L)).thenReturn(primaryNetwork); + VMTemplateVO template = Mockito.mock(VMTemplateVO.class); + Mockito.when(templateDao.findByIdIncludingRemoved(99L)).thenReturn(template); + Mockito.doThrow(new InvalidParameterValueException("VNF nic is the management interface")) + .when(vnfTemplateManagerMock).validateVnfApplianceTrunkNic(template, 0L); + + service.associateNetworksToNic(nic, Arrays.asList(206L), null); + } + + @Test(expected = InvalidParameterValueException.class) + public void associateNetworksToNicRejectsNetworkAlreadyOnAnotherNic() throws Exception { + // vm has a second, ordinary nic (208L) whose primary is network 206L - trying to associate 206L onto + // nic 11L too must be rejected, since a network can only be reachable through one of the vm's nics + NicVO nic = mockNic(11L, 5L, 205L, false); + Mockito.when(nicDao.findById(11L)).thenReturn(nic); + UserVmVO vm = mockVm(VirtualMachine.State.Stopped, null); + Mockito.when(vm.getId()).thenReturn(5L); + Mockito.when(userVmDao.findById(5L)).thenReturn(vm); + NetworkVO primaryNetwork = mockNetwork(205L, Network.GuestType.Isolated, 1L, 1L, null, Networks.BroadcastDomainType.Vlan, vlanUri(1174)); + Mockito.when(networkDao.findById(205L)).thenReturn(primaryNetwork); + Mockito.when(nicNetworkMapDao.listByNicId(11L)).thenReturn(new ArrayList<>()); + + NicVO otherNic = mockNic(12L, 5L, 206L, false); + Mockito.when(nicDao.listByVmId(5L)).thenReturn(Arrays.asList(nic, otherNic)); + Mockito.when(nicNetworkMapDao.listByNicId(12L)).thenReturn(new ArrayList<>()); + + service.associateNetworksToNic(nic, Arrays.asList(206L), null); + } + + @Test + public void associateNetworksToNicPublishesBackfillEventOnFirstConversionToTrunk() throws Exception { + // nic is not yet a trunk nic (multiNetwork=false) - this is the first association, so the nic's own + // still-open, pre-existing usage row(s) for its primary network need a one-time network_id backfill. + // The backfill itself runs later, on the usage job's own thread (see UsageManagerImpl) - this request + // thread only ever publishes the event, since it already has an ambient `cloud` DB transaction open + // and cloud_usage is a separate database. + NicVO nic = mockNic(11L, 5L, 205L, false); + Mockito.when(nicDao.findById(11L)).thenReturn(nic); + UserVmVO vm = mockVm(VirtualMachine.State.Stopped, null); + Mockito.when(userVmDao.findById(5L)).thenReturn(vm); + NetworkVO primaryNetwork = mockNetwork(205L, Network.GuestType.Isolated, 1L, 1L, null, Networks.BroadcastDomainType.Vlan, vlanUri(1174)); + Mockito.when(networkDao.findById(205L)).thenReturn(primaryNetwork); + Mockito.when(nicNetworkMapDao.listByNicId(11L)).thenReturn(new ArrayList<>()); + NetworkVO requestedNetwork = mockNetwork(206L, Network.GuestType.Isolated, 1L, 1L, null, Networks.BroadcastDomainType.Vlan, vlanUri(1180)); + Mockito.when(networkDao.findById(206L)).thenReturn(requestedNetwork); + Mockito.when(ipAddressManagerMock.allocateGuestIP(requestedNetwork, null)).thenReturn("10.1.1.60"); + + service.associateNetworksToNic(nic, Arrays.asList(206L), null); + + usageEventUtilsMocked.verify(() -> UsageEventUtils.publishUsageEvent( + Mockito.eq(EventTypes.EVENT_NIC_NETWORK_ID_BACKFILL), Mockito.anyLong(), Mockito.anyLong(), Mockito.eq(11L), Mockito.eq("11"), + Mockito.isNull(), Mockito.isNull(), Mockito.isNull(), Mockito.anyString(), Mockito.any(), + Mockito.argThat(details -> "205".equals(details.get(UsageEventVO.DynamicParameters.networkId.name())))), Mockito.times(1)); + } + + @Test + public void associateNetworksToNicDoesNotPublishBackfillEventWhenNicIsAlreadyATrunkNic() throws Exception { + // nic is already a trunk nic - this is a further association on top of an existing one, not the + // first-time conversion, so the one-time backfill must not fire again + NicVO nic = mockNic(11L, 5L, 205L, true); + Mockito.when(nicDao.findById(11L)).thenReturn(nic); + UserVmVO vm = mockVm(VirtualMachine.State.Stopped, null); + Mockito.when(userVmDao.findById(5L)).thenReturn(vm); + NetworkVO primaryNetwork = mockNetwork(205L, Network.GuestType.Isolated, 1L, 1L, null, Networks.BroadcastDomainType.Vlan, vlanUri(1174)); + Mockito.when(networkDao.findById(205L)).thenReturn(primaryNetwork); + Mockito.when(nicNetworkMapDao.listByNicId(11L)).thenReturn(new ArrayList<>()); + NetworkVO requestedNetwork = mockNetwork(207L, Network.GuestType.Isolated, 1L, 1L, null, Networks.BroadcastDomainType.Vlan, vlanUri(1190)); + Mockito.when(networkDao.findById(207L)).thenReturn(requestedNetwork); + Mockito.when(ipAddressManagerMock.allocateGuestIP(requestedNetwork, null)).thenReturn("10.1.1.70"); + + service.associateNetworksToNic(nic, Arrays.asList(207L), null); + + usageEventUtilsMocked.verify(() -> UsageEventUtils.publishUsageEvent( + Mockito.eq(EventTypes.EVENT_NIC_NETWORK_ID_BACKFILL), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyString(), + Mockito.isNull(), Mockito.isNull(), Mockito.isNull(), Mockito.anyString(), Mockito.any(), Mockito.anyMap()), Mockito.never()); + } + + @Test(expected = CloudRuntimeException.class) + public void associateNetworksToNicFailsWhenVmNotStopped() throws Exception { + NicVO nic = mockNic(11L, 5L, 205L, false); + Mockito.when(nicDao.findById(11L)).thenReturn(nic); + UserVmVO vm = mockVm(VirtualMachine.State.Running, 1L); + Mockito.when(userVmDao.findById(5L)).thenReturn(vm); + + service.associateNetworksToNic(nic, Arrays.asList(206L), null); + } + + private DisassociateNetworkFromNicCmd mockDisassociateCmd(long nicId, long networkId) { + DisassociateNetworkFromNicCmd cmd = Mockito.mock(DisassociateNetworkFromNicCmd.class); + Mockito.when(cmd.getNicId()).thenReturn(nicId); + Mockito.when(cmd.getNetworkId()).thenReturn(networkId); + return cmd; + } + + @Test + public void disassociateNetworkFromNicFailsWhenFeatureDisabledInZone() throws Exception { + NicVO nic = mockNic(11L, 5L, 205L, true); + Mockito.when(nicDao.findById(11L)).thenReturn(nic); + UserVmVO vm = mockVm(VirtualMachine.State.Stopped, null); + Mockito.when(userVmDao.findById(5L)).thenReturn(vm); + + overrideDefaultConfigValue(NetworkOrchestrationService.MultiNetworkNicEnabled, "_defaultValue", "false"); + try { + service.disassociateNetworkFromNic(mockDisassociateCmd(11L, 206L)); + Assert.fail("expected InvalidParameterValueException"); + } catch (InvalidParameterValueException e) { + Assert.assertTrue(e.getMessage().contains("not enabled")); + } finally { + overrideDefaultConfigValue(NetworkOrchestrationService.MultiNetworkNicEnabled, "_defaultValue", "true"); + } + } + + @Test(expected = InvalidParameterValueException.class) + public void disassociateNetworkFromNicFailsWhenTargetIsPrimaryNetwork() throws Exception { + NicVO nic = mockNic(11L, 5L, 205L, true); + Mockito.when(nicDao.findById(11L)).thenReturn(nic); + UserVmVO vm = mockVm(VirtualMachine.State.Stopped, null); + Mockito.when(userVmDao.findById(5L)).thenReturn(vm); + + service.disassociateNetworkFromNic(mockDisassociateCmd(11L, 205L)); + } + + @Test(expected = InvalidParameterValueException.class) + public void disassociateNetworkFromNicFailsWhenNotAssociated() throws Exception { + NicVO nic = mockNic(11L, 5L, 205L, true); + Mockito.when(nicDao.findById(11L)).thenReturn(nic); + UserVmVO vm = mockVm(VirtualMachine.State.Stopped, null); + Mockito.when(userVmDao.findById(5L)).thenReturn(vm); + Mockito.when(nicNetworkMapDao.findByNicIdAndNetworkId(11L, 206L)).thenReturn(null); + + service.disassociateNetworkFromNic(mockDisassociateCmd(11L, 206L)); + } + + @Test(expected = InvalidParameterValueException.class) + public void disassociateNetworkFromNicFailsWhenAssociationIpHasActiveStaticNat() throws Exception { + NicVO nic = mockNic(11L, 5L, 205L, true); + Mockito.when(nicDao.findById(11L)).thenReturn(nic); + UserVmVO vm = mockVm(VirtualMachine.State.Stopped, null); + Mockito.when(vm.getId()).thenReturn(5L); + Mockito.when(userVmDao.findById(5L)).thenReturn(vm); + NicNetworkMapVO association = new NicNetworkMapVO(11L, 206L, "10.3.1.25", null); + Mockito.when(nicNetworkMapDao.findByNicIdAndNetworkId(11L, 206L)).thenReturn(association); + IPAddressVO staticNatIp = Mockito.mock(IPAddressVO.class); + Mockito.when(ipAddressDao.findByAssociatedVmIdAndVmIp(5L, "10.3.1.25")).thenReturn(staticNatIp); + + service.disassociateNetworkFromNic(mockDisassociateCmd(11L, 206L)); + } + + @Test + public void disassociateNetworkFromNicRemovesAssociationOnAStoppedVm() throws Exception { + NicVO nic = mockNic(11L, 5L, 205L, true); + Mockito.when(nicDao.findById(11L)).thenReturn(nic); + UserVmVO vm = mockVm(VirtualMachine.State.Stopped, null); + Mockito.when(userVmDao.findById(5L)).thenReturn(vm); + NicNetworkMapVO association = new NicNetworkMapVO(11L, 206L, "10.1.1.50", null); + Mockito.when(nicNetworkMapDao.findByNicIdAndNetworkId(11L, 206L)).thenReturn(association); + Mockito.when(nicNetworkMapDao.listByNicId(11L)).thenReturn(Collections.emptyList()); + + Nic result = service.disassociateNetworkFromNic(mockDisassociateCmd(11L, 206L)); + + Assert.assertNotNull(result); + Mockito.verify(nicNetworkMapDao).remove(association.getId()); + Mockito.verify(agentMgr, Mockito.never()).easySend(Mockito.anyLong(), Mockito.any()); + Mockito.verify(nic).setMultiNetwork(false); + Mockito.verify(nicDao).update(11L, nic); + } + + @Test + public void disassociateNetworkFromNicKeepsMultiNetworkFlagWhenOtherAssociationsRemain() throws Exception { + NicVO nic = mockNic(11L, 5L, 205L, true); + Mockito.when(nicDao.findById(11L)).thenReturn(nic); + UserVmVO vm = mockVm(VirtualMachine.State.Stopped, null); + Mockito.when(userVmDao.findById(5L)).thenReturn(vm); + NicNetworkMapVO association = new NicNetworkMapVO(11L, 206L, "10.1.1.50", null); + Mockito.when(nicNetworkMapDao.findByNicIdAndNetworkId(11L, 206L)).thenReturn(association); + NicNetworkMapVO remainingAssociation = new NicNetworkMapVO(11L, 207L, "10.1.1.60", null); + Mockito.when(nicNetworkMapDao.listByNicId(11L)).thenReturn(Arrays.asList(remainingAssociation)); + + Nic result = service.disassociateNetworkFromNic(mockDisassociateCmd(11L, 206L)); + + Assert.assertNotNull(result); + Mockito.verify(nicNetworkMapDao).remove(association.getId()); + Mockito.verify(nic, Mockito.never()).setMultiNetwork(Mockito.anyBoolean()); + Mockito.verify(nicDao, Mockito.never()).update(Mockito.eq(11L), Mockito.any()); + } + + @Test + public void disassociateNetworkFromNicUpdatesLiveVlanMembershipOnARunningVm() throws Exception { + NicVO nic = mockNic(11L, 5L, 205L, true); + Mockito.when(nicDao.findById(11L)).thenReturn(nic); + UserVmVO vm = mockVm(VirtualMachine.State.Running, 1L); + Mockito.when(vm.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.KVM); + Mockito.when(vm.getInstanceName()).thenReturn("i-2-11-VM"); + Mockito.when(userVmDao.findById(5L)).thenReturn(vm); + DetailVO readyDetail = Mockito.mock(DetailVO.class); + Mockito.when(readyDetail.getValue()).thenReturn("true"); + Mockito.when(hostDetailsDao.findDetail(1L, Host.HOST_VLAN_FILTERING_ENABLED)).thenReturn(readyDetail); + NicNetworkMapVO association = new NicNetworkMapVO(11L, 206L, "10.1.1.50", null); + Mockito.when(nicNetworkMapDao.findByNicIdAndNetworkId(11L, 206L)).thenReturn(association); + Mockito.when(networkModel.getNicProfile(Mockito.eq(vm), Mockito.eq(nic), Mockito.any())).thenReturn(null); + Mockito.when(hvGuruMgr.getGuru(Hypervisor.HypervisorType.KVM)).thenReturn(hypervisorGuru); + Mockito.when(hypervisorGuru.toNicTO(null)).thenReturn(Mockito.mock(NicTO.class)); + Mockito.when(agentMgr.easySend(Mockito.eq(1L), Mockito.any())).thenReturn(new UpdateNicVlanMembershipAnswer(null, true, "success")); + DomainRouterVO router = Mockito.mock(DomainRouterVO.class); + Mockito.when(router.getState()).thenReturn(VirtualMachine.State.Running); + Mockito.when(routerDao.findByNetwork(205L)).thenReturn(Arrays.asList(router)); + + Nic result = service.disassociateNetworkFromNic(mockDisassociateCmd(11L, 206L)); + + Assert.assertNotNull(result); + Mockito.verify(nicNetworkMapDao).remove(association.getId()); + Mockito.verify(agentMgr).easySend(Mockito.eq(1L), Mockito.any()); + Mockito.verify(commandSetupHelper).createVmDataCommand(Mockito.eq(router), Mockito.eq(vm), Mockito.eq(nic), Mockito.any(), Mockito.any()); + Mockito.verify(networkHelper).sendCommandsToRouter(Mockito.eq(router), Mockito.any()); + } + + @Test + public void disassociateNetworkFromNicRemovesDhcpEntryOnARunningVm() throws Exception { + NicVO nic = mockNic(11L, 5L, 205L, true); + Mockito.when(nicDao.findById(11L)).thenReturn(nic); + UserVmVO vm = mockVm(VirtualMachine.State.Running, 1L); + Mockito.when(vm.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.KVM); + Mockito.when(vm.getInstanceName()).thenReturn("i-2-11-VM"); + Mockito.when(userVmDao.findById(5L)).thenReturn(vm); + DetailVO readyDetail = Mockito.mock(DetailVO.class); + Mockito.when(readyDetail.getValue()).thenReturn("true"); + Mockito.when(hostDetailsDao.findDetail(1L, Host.HOST_VLAN_FILTERING_ENABLED)).thenReturn(readyDetail); + NicNetworkMapVO association = new NicNetworkMapVO(11L, 206L, "10.1.1.50", null); + Mockito.when(nicNetworkMapDao.findByNicIdAndNetworkId(11L, 206L)).thenReturn(association); + NetworkVO associatedNetwork = mockNetwork(206L, Network.GuestType.Isolated, 1L, 1L, null, Networks.BroadcastDomainType.Vlan, vlanUri(1180)); + Mockito.when(networkDao.findById(206L)).thenReturn(associatedNetwork); + Mockito.when(networkModel.getNicProfile(Mockito.eq(vm), Mockito.eq(nic), Mockito.any())).thenReturn(null); + Mockito.when(hvGuruMgr.getGuru(Hypervisor.HypervisorType.KVM)).thenReturn(hypervisorGuru); + Mockito.when(hypervisorGuru.toNicTO(null)).thenReturn(Mockito.mock(NicTO.class)); + Mockito.when(agentMgr.easySend(Mockito.eq(1L), Mockito.any())).thenReturn(new UpdateNicVlanMembershipAnswer(null, true, "success")); + Mockito.when(routerDao.findByNetwork(205L)).thenReturn(new ArrayList<>()); + DomainRouterVO associatedNetworkRouter = Mockito.mock(DomainRouterVO.class); + Mockito.when(associatedNetworkRouter.getState()).thenReturn(VirtualMachine.State.Running); + Mockito.when(routerDao.findByNetwork(206L)).thenReturn(Arrays.asList(associatedNetworkRouter)); + + service.disassociateNetworkFromNic(mockDisassociateCmd(11L, 206L)); + + Mockito.verify(commandSetupHelper).createDhcpEntryCommand(Mockito.eq(associatedNetworkRouter), Mockito.eq(vm), Mockito.any(), Mockito.eq("10.1.1.50"), + Mockito.any(), Mockito.any(), Mockito.any(), Mockito.eq(206L), Mockito.eq(false), Mockito.eq(true), Mockito.any()); + Mockito.verify(networkHelper).sendCommandsToRouter(Mockito.eq(associatedNetworkRouter), Mockito.any()); + } + + @Test + public void disassociateNetworkFromNicDoesNotRemoveDhcpEntryOnAStoppedVm() throws Exception { + NicVO nic = mockNic(11L, 5L, 205L, true); + Mockito.when(nicDao.findById(11L)).thenReturn(nic); + UserVmVO vm = mockVm(VirtualMachine.State.Stopped, null); + Mockito.when(userVmDao.findById(5L)).thenReturn(vm); + NicNetworkMapVO association = new NicNetworkMapVO(11L, 206L, "10.1.1.50", null); + Mockito.when(nicNetworkMapDao.findByNicIdAndNetworkId(11L, 206L)).thenReturn(association); + + service.disassociateNetworkFromNic(mockDisassociateCmd(11L, 206L)); + + Mockito.verify(commandSetupHelper, Mockito.never()).createDhcpEntryCommand(Mockito.any(), Mockito.any(), Mockito.anyString(), Mockito.anyString(), + Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyLong(), Mockito.anyBoolean(), Mockito.anyBoolean(), Mockito.any()); + } + + @Test + public void disassociateNetworkFromNicSucceedsWhenMetadataRefreshFails() throws Exception { + NicVO nic = mockNic(11L, 5L, 205L, true); + Mockito.when(nicDao.findById(11L)).thenReturn(nic); + UserVmVO vm = mockVm(VirtualMachine.State.Running, 1L); + Mockito.when(vm.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.KVM); + Mockito.when(vm.getInstanceName()).thenReturn("i-2-11-VM"); + Mockito.when(userVmDao.findById(5L)).thenReturn(vm); + DetailVO readyDetail = Mockito.mock(DetailVO.class); + Mockito.when(readyDetail.getValue()).thenReturn("true"); + Mockito.when(hostDetailsDao.findDetail(1L, Host.HOST_VLAN_FILTERING_ENABLED)).thenReturn(readyDetail); + NicNetworkMapVO association = new NicNetworkMapVO(11L, 206L, "10.1.1.50", null); + Mockito.when(nicNetworkMapDao.findByNicIdAndNetworkId(11L, 206L)).thenReturn(association); + Mockito.when(networkModel.getNicProfile(Mockito.eq(vm), Mockito.eq(nic), Mockito.any())).thenReturn(null); + Mockito.when(hvGuruMgr.getGuru(Hypervisor.HypervisorType.KVM)).thenReturn(hypervisorGuru); + Mockito.when(hypervisorGuru.toNicTO(null)).thenReturn(Mockito.mock(NicTO.class)); + Mockito.when(agentMgr.easySend(Mockito.eq(1L), Mockito.any())).thenReturn(new UpdateNicVlanMembershipAnswer(null, true, "success")); + DomainRouterVO router = Mockito.mock(DomainRouterVO.class); + Mockito.when(router.getState()).thenReturn(VirtualMachine.State.Running); + Mockito.when(routerDao.findByNetwork(205L)).thenReturn(Arrays.asList(router)); + Mockito.when(networkHelper.sendCommandsToRouter(Mockito.eq(router), Mockito.any())).thenThrow(new CloudRuntimeException("router unreachable")); + + Nic result = service.disassociateNetworkFromNic(mockDisassociateCmd(11L, 206L)); + + Assert.assertNotNull(result); + Mockito.verify(nicNetworkMapDao).remove(association.getId()); + } + + private ChangeNicPrimaryNetworkCmd mockChangePrimaryCmd(long nicId, long networkId) { + ChangeNicPrimaryNetworkCmd cmd = Mockito.mock(ChangeNicPrimaryNetworkCmd.class); + Mockito.when(cmd.getNicId()).thenReturn(nicId); + Mockito.when(cmd.getNetworkId()).thenReturn(networkId); + return cmd; + } + + @Test + public void changeNicPrimaryNetworkFailsWhenFeatureDisabledInZone() throws Exception { + NicVO nic = mockNic(11L, 5L, 205L, true); + Mockito.when(nicDao.findById(11L)).thenReturn(nic); + UserVmVO vm = mockVm(VirtualMachine.State.Stopped, null); + Mockito.when(userVmDao.findById(5L)).thenReturn(vm); + + overrideDefaultConfigValue(NetworkOrchestrationService.MultiNetworkNicEnabled, "_defaultValue", "false"); + try { + service.changeNicPrimaryNetwork(mockChangePrimaryCmd(11L, 206L)); + Assert.fail("expected InvalidParameterValueException"); + } catch (InvalidParameterValueException e) { + Assert.assertTrue(e.getMessage().contains("not enabled")); + } finally { + overrideDefaultConfigValue(NetworkOrchestrationService.MultiNetworkNicEnabled, "_defaultValue", "true"); + } + } + + @Test(expected = InvalidParameterValueException.class) + public void changeNicPrimaryNetworkFailsWhenVmNotStopped() throws Exception { + NicVO nic = mockNic(11L, 5L, 205L, true); + Mockito.when(nicDao.findById(11L)).thenReturn(nic); + UserVmVO vm = mockVm(VirtualMachine.State.Running, 1L); + Mockito.when(userVmDao.findById(5L)).thenReturn(vm); + + service.changeNicPrimaryNetwork(mockChangePrimaryCmd(11L, 206L)); + } + + @Test(expected = InvalidParameterValueException.class) + public void changeNicPrimaryNetworkFailsWhenTargetIsCurrentPrimary() throws Exception { + NicVO nic = mockNic(11L, 5L, 205L, true); + Mockito.when(nicDao.findById(11L)).thenReturn(nic); + UserVmVO vm = mockVm(VirtualMachine.State.Stopped, null); + Mockito.when(userVmDao.findById(5L)).thenReturn(vm); + + service.changeNicPrimaryNetwork(mockChangePrimaryCmd(11L, 205L)); + } + + @Test(expected = InvalidParameterValueException.class) + public void changeNicPrimaryNetworkFailsWhenTargetNotAssociated() throws Exception { + NicVO nic = mockNic(11L, 5L, 205L, true); + Mockito.when(nicDao.findById(11L)).thenReturn(nic); + UserVmVO vm = mockVm(VirtualMachine.State.Stopped, null); + Mockito.when(userVmDao.findById(5L)).thenReturn(vm); + Mockito.when(nicNetworkMapDao.findByNicIdAndNetworkId(11L, 206L)).thenReturn(null); + + service.changeNicPrimaryNetwork(mockChangePrimaryCmd(11L, 206L)); + } + + @Test + public void changeNicPrimaryNetworkSwapsPrimaryAndDemotesOldPrimaryToAssociation() throws Exception { + NicVO nic = mockNic(11L, 5L, 205L, true); + Mockito.when(nic.getIPv4Address()).thenReturn("10.1.1.10"); + Mockito.when(nic.getIPv6Address()).thenReturn(null); + Mockito.when(nicDao.findById(11L)).thenReturn(nic); + UserVmVO vm = mockVm(VirtualMachine.State.Stopped, null); + Mockito.when(userVmDao.findById(5L)).thenReturn(vm); + NicNetworkMapVO association = new NicNetworkMapVO(11L, 206L, "10.1.1.50", null); + Mockito.when(nicNetworkMapDao.findByNicIdAndNetworkId(11L, 206L)).thenReturn(association); + NetworkVO newPrimaryNetwork = mockNetwork(206L, Network.GuestType.Isolated, 1L, 1L, null, Networks.BroadcastDomainType.Vlan, vlanUri(1180)); + Mockito.when(newPrimaryNetwork.getGateway()).thenReturn("10.1.2.1"); + Mockito.when(newPrimaryNetwork.getCidr()).thenReturn("10.1.2.0/24"); + Mockito.when(networkDao.findById(206L)).thenReturn(newPrimaryNetwork); + Mockito.when(networkModel.getValidNetworkCidr(newPrimaryNetwork)).thenReturn("10.1.2.0/24"); + + Nic result = service.changeNicPrimaryNetwork(mockChangePrimaryCmd(11L, 206L)); + + Assert.assertNotNull(result); + Mockito.verify(nicNetworkMapDao).remove(association.getId()); + Mockito.verify(nicNetworkMapDao).persist(Mockito.argThat(map -> + map.getNicId() == 11L && map.getNetworkId() == 205L && "10.1.1.10".equals(map.getIp4Address()))); + Mockito.verify(nic).setNetworkId(206L); + Mockito.verify(nic).setIPv4Address("10.1.1.50"); + Mockito.verify(nic).setIPv6Address(null); + Mockito.verify(nic).setIPv4Gateway("10.1.2.1"); + Mockito.verify(nic).setIPv4Netmask(NetUtils.cidr2Netmask("10.1.2.0/24")); + Mockito.verify(nic).setBroadcastUri(vlanUri(1180)); + Mockito.verify(nic).setIsolationUri(vlanUri(1180)); + Mockito.verify(nicDao).update(Mockito.eq(11L), Mockito.eq(nic)); + } + + private URI vlanUri(int vlanId) { + return Networks.BroadcastDomainType.Vlan.toUri(vlanId); + } } diff --git a/server/src/test/java/com/cloud/network/lb/AssignLoadBalancerTest.java b/server/src/test/java/com/cloud/network/lb/AssignLoadBalancerTest.java index 1fc0517a727e..cbbda1f2ee74 100644 --- a/server/src/test/java/com/cloud/network/lb/AssignLoadBalancerTest.java +++ b/server/src/test/java/com/cloud/network/lb/AssignLoadBalancerTest.java @@ -40,6 +40,7 @@ import com.cloud.user.UserVO; import com.cloud.user.dao.AccountDao; import com.cloud.uservm.UserVm; +import com.cloud.utils.Pair; import com.cloud.vm.Nic; import com.cloud.vm.NicVO; import com.cloud.vm.UserVmVO; @@ -102,6 +103,15 @@ public List getNics(long vmId) { nic.add(nicvo); return nic; } + + @Override + public Pair getNicAndIpInNetwork(long vmId, long networkId) { + nicvo.setNetworkId(204L); + if (networkId != 204L) { + return null; + } + return new Pair<>(nicvo, new Network.IpAddresses(nicvo.getIPv4Address(), nicvo.getIPv6Address())); + } }; diff --git a/server/src/test/java/com/cloud/network/lb/LoadBalancingRulesManagerImplTest.java b/server/src/test/java/com/cloud/network/lb/LoadBalancingRulesManagerImplTest.java index 282d892600b5..deca36df56f9 100644 --- a/server/src/test/java/com/cloud/network/lb/LoadBalancingRulesManagerImplTest.java +++ b/server/src/test/java/com/cloud/network/lb/LoadBalancingRulesManagerImplTest.java @@ -24,7 +24,6 @@ import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -66,6 +65,7 @@ import com.cloud.user.User; import com.cloud.user.UserVO; import com.cloud.uservm.UserVm; +import com.cloud.utils.Pair; import com.cloud.utils.db.EntityManager; import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.utils.net.NetUtils; @@ -332,11 +332,10 @@ public void testGetVmNicInLoadBalancerDefaultCase() { when(loadBalancer.getNetworkId()).thenReturn(networkId); Nic nic = Mockito.mock(Nic.class); - when(nic.getNetworkId()).thenReturn(networkId); - List nics = Collections.singletonList(nic); - Mockito.doReturn(nics).when(_networkModel).getNics(anyLong()); - Nic nicInLb = lbr.getVmNicInLoadBalancer(userVm, loadBalancer, loadBalancerNetwork, null, owner); - Assert.assertEquals(nic, nicInLb); + Pair nicAndIp = new Pair<>(nic, new Network.IpAddresses("10.1.1.5", null)); + Mockito.doReturn(nicAndIp).when(_networkModel).getNicAndIpInNetwork(anyLong(), Mockito.eq(networkId)); + Pair nicInLb = lbr.getVmNicInLoadBalancer(userVm, loadBalancer, loadBalancerNetwork, null, owner); + Assert.assertEquals(nic, nicInLb.first()); } @Test @@ -358,10 +357,11 @@ public void testGetVmNicInLoadBalancerVPCConserveMode() { when(networkTier2.getVpcId()).thenReturn(10L); when(loadBalancerNetwork.getVpcId()).thenReturn(10L); Nic nic = Mockito.mock(Nic.class); - when(_networkModel.getNicInNetwork(Mockito.eq(vmId), Mockito.eq(networkTier2Id))).thenReturn(nic); + Pair nicAndIp = new Pair<>(nic, new Network.IpAddresses("10.1.1.5", null)); + when(_networkModel.getNicAndIpInNetwork(Mockito.eq(vmId), Mockito.eq(networkTier2Id))).thenReturn(nicAndIp); - Nic nicInLb = lbr.getVmNicInLoadBalancer(userVm, loadBalancer, loadBalancerNetwork, vmIdNetworkIdMap, owner); - Assert.assertEquals(nic, nicInLb); + Pair nicInLb = lbr.getVmNicInLoadBalancer(userVm, loadBalancer, loadBalancerNetwork, vmIdNetworkIdMap, owner); + Assert.assertEquals(nic, nicInLb.first()); } @Test(expected = InvalidParameterValueException.class) diff --git a/server/src/test/java/com/cloud/network/router/CommandSetupHelperTest.java b/server/src/test/java/com/cloud/network/router/CommandSetupHelperTest.java index 34909e7e860f..2e7b1e33df88 100644 --- a/server/src/test/java/com/cloud/network/router/CommandSetupHelperTest.java +++ b/server/src/test/java/com/cloud/network/router/CommandSetupHelperTest.java @@ -42,14 +42,21 @@ import com.cloud.offerings.NetworkOfferingVO; import com.cloud.offerings.dao.NetworkOfferingDao; import com.cloud.offerings.dao.NetworkOfferingDetailsDao; +import com.cloud.network.Networks.BroadcastDomainType; +import com.cloud.uservm.UserVm; import com.cloud.utils.net.Ip; import com.cloud.vm.NicVO; import com.cloud.vm.VirtualMachine; +import com.cloud.vm.VirtualMachineManager; import com.cloud.vm.dao.NicDao; +import com.cloud.vm.dao.NicNetworkMapDao; +import com.cloud.vm.dao.NicNetworkMapVO; import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService; import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.framework.config.impl.ConfigDepotImpl; import org.apache.cloudstack.network.BgpPeerVO; import org.apache.cloudstack.network.dao.BgpPeerDetailsDao; +import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -63,6 +70,7 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -102,10 +110,13 @@ public class CommandSetupHelperTest { ASNumberDao asNumberDao; @Mock BgpPeerDetailsDao bgpPeerDetailsDao; + @Mock + NicNetworkMapDao nicNetworkMapDao; @Before public void setUp() { ReflectionTestUtils.setField(commandSetupHelper, "_nicDao", nicDao); + ReflectionTestUtils.setField(commandSetupHelper, "_nicNetworkMapDao", nicNetworkMapDao); ReflectionTestUtils.setField(commandSetupHelper, "_networkDao", networkDao); ReflectionTestUtils.setField(commandSetupHelper, "_ipAddressDao", ipAddressDao); ReflectionTestUtils.setField(commandSetupHelper, "_vlanDao", vlanDao); @@ -118,6 +129,125 @@ public void setUp() { ReflectionTestUtils.setField(commandSetupHelper, "_dcDao", dcDao); } + @After + public void tearDown() { + ConfigKey.init(null); + } + + private NetworkVO createVlanNetwork(String uuid, String name, long vlanTag) { + NetworkVO network = Mockito.mock(NetworkVO.class); + when(network.getUuid()).thenReturn(uuid); + when(network.getName()).thenReturn(name); + when(network.getBroadcastDomainType()).thenReturn(BroadcastDomainType.Vlan); + when(network.getBroadcastUri()).thenReturn(BroadcastDomainType.Vlan.toUri(vlanTag)); + return network; + } + + private void enableNicVlanMappingExposure() { + ConfigDepotImpl configDepotMock = Mockito.mock(ConfigDepotImpl.class); + ConfigKey.init(configDepotMock); + when(configDepotMock.getConfigStringValue(Mockito.eq(VirtualMachineManager.AllowExposeNicVlanMapping.toString()), Mockito.any(), Mockito.any())) + .thenReturn("true"); + } + + @Test + public void testAddNicVlanMappingToVmDataSkipsWhenNicNotTrunked() { + VmDataCommand vmDataCommand = new VmDataCommand("testVMname"); + NicVO nic = Mockito.mock(NicVO.class); + when(nic.getMultiNetwork()).thenReturn(false); + UserVm vm = Mockito.mock(UserVm.class); + + commandSetupHelper.addNicVlanMappingToVmData(vmDataCommand, vm, nic); + + Assert.assertTrue(vmDataCommand.getVmData().isEmpty()); + Mockito.verifyNoInteractions(nicNetworkMapDao); + } + + @Test + public void testAddNicVlanMappingToVmDataSkipsWhenConfigDisabled() { + VmDataCommand vmDataCommand = new VmDataCommand("testVMname"); + NicVO nic = Mockito.mock(NicVO.class); + when(nic.getMultiNetwork()).thenReturn(true); + UserVm vm = Mockito.mock(UserVm.class); + when(vm.getAccountId()).thenReturn(2L); + + commandSetupHelper.addNicVlanMappingToVmData(vmDataCommand, vm, nic); + + Assert.assertTrue(vmDataCommand.getVmData().isEmpty()); + Mockito.verifyNoInteractions(nicNetworkMapDao); + } + + @Test + public void testAddNicVlanMappingToVmDataClearsFileWhenNoAssociations() { + enableNicVlanMappingExposure(); + VmDataCommand vmDataCommand = new VmDataCommand("testVMname"); + NicVO nic = Mockito.mock(NicVO.class); + when(nic.getId()).thenReturn(11L); + when(nic.getMultiNetwork()).thenReturn(true); + UserVm vm = Mockito.mock(UserVm.class); + when(vm.getAccountId()).thenReturn(2L); + when(nicNetworkMapDao.listByNicId(11L)).thenReturn(Collections.emptyList()); + + commandSetupHelper.addNicVlanMappingToVmData(vmDataCommand, vm, nic); + + List metadata = vmDataCommand.getVmData(); + Assert.assertEquals(1, metadata.size()); + String[] metadataFile = metadata.get(0); + Assert.assertEquals("metadata", metadataFile[0]); + Assert.assertEquals(NetworkModel.NIC_VLAN_MAPPING_FILE, metadataFile[1]); + Assert.assertEquals("", metadataFile[2]); + } + + @Test + public void testAddNicVlanMappingToVmDataAddsMappingsWhenEnabledAndTrunked() { + enableNicVlanMappingExposure(); + VmDataCommand vmDataCommand = new VmDataCommand("testVMname"); + NicVO nic = Mockito.mock(NicVO.class); + when(nic.getId()).thenReturn(11L); + when(nic.getMultiNetwork()).thenReturn(true); + UserVm vm = Mockito.mock(UserVm.class); + when(vm.getAccountId()).thenReturn(2L); + + NicNetworkMapVO association = new NicNetworkMapVO(11L, 205L); + when(nicNetworkMapDao.listByNicId(11L)).thenReturn(Arrays.asList(association)); + NetworkVO associatedNetwork = createVlanNetwork("network-uuid-205", "netNormal", 1179L); + when(networkDao.findById(205L)).thenReturn(associatedNetwork); + + commandSetupHelper.addNicVlanMappingToVmData(vmDataCommand, vm, nic); + + List metadata = vmDataCommand.getVmData(); + Assert.assertEquals(1, metadata.size()); + String[] metadataFile = metadata.get(0); + Assert.assertEquals("metadata", metadataFile[0]); + Assert.assertEquals(NetworkModel.NIC_VLAN_MAPPING_FILE, metadataFile[1]); + Assert.assertTrue(metadataFile[2].contains("network-uuid-205")); + Assert.assertTrue(metadataFile[2].contains("netNormal")); + Assert.assertTrue(metadataFile[2].contains("1179")); + } + + @Test + public void testAddNicVlanMappingToVmDataClearsFileWhenOnlyNonVlanAssociatedNetworks() { + enableNicVlanMappingExposure(); + VmDataCommand vmDataCommand = new VmDataCommand("testVMname"); + NicVO nic = Mockito.mock(NicVO.class); + when(nic.getId()).thenReturn(11L); + when(nic.getMultiNetwork()).thenReturn(true); + UserVm vm = Mockito.mock(UserVm.class); + when(vm.getAccountId()).thenReturn(2L); + + NicNetworkMapVO association = new NicNetworkMapVO(11L, 206L); + when(nicNetworkMapDao.listByNicId(11L)).thenReturn(Arrays.asList(association)); + NetworkVO nonVlanNetwork = Mockito.mock(NetworkVO.class); + when(nonVlanNetwork.getBroadcastDomainType()).thenReturn(BroadcastDomainType.Lswitch); + when(networkDao.findById(206L)).thenReturn(nonVlanNetwork); + + commandSetupHelper.addNicVlanMappingToVmData(vmDataCommand, vm, nic); + + List metadata = vmDataCommand.getVmData(); + Assert.assertEquals(1, metadata.size()); + Assert.assertEquals("", metadata.get(0)[2]); + } + @Test public void testUserDataDetails() { VmDataCommand vmDataCommand = new VmDataCommand("testVMname"); diff --git a/server/src/test/java/com/cloud/server/ManagementServerImplTest.java b/server/src/test/java/com/cloud/server/ManagementServerImplTest.java index d2c12b6c99dd..74c9c5e66821 100644 --- a/server/src/test/java/com/cloud/server/ManagementServerImplTest.java +++ b/server/src/test/java/com/cloud/server/ManagementServerImplTest.java @@ -310,6 +310,19 @@ private void overrideDefaultConfigValue(final ConfigKey configKey, final String f.set(configKey, o); } + @Test + public void applyAffinityConstraintsChecksMultiNetworkNicHostReadiness() { + Mockito.when(virtualMachineMock.getId()).thenReturn(1L); + Mockito.when(virtualMachineMock.getHostId()).thenReturn(5L); + Mockito.when(dataCenterDeploymentMock.getDataCenterId()).thenReturn(2L); + DataCenterVO dc = Mockito.mock(DataCenterVO.class); + Mockito.when(dcDao.findById(2L)).thenReturn(dc); + + spy.applyAffinityConstraints(virtualMachineMock, virtualMachineProfileMock, dataCenterDeploymentMock, Collections.emptyList()); + + Mockito.verify(dpMgr).avoidHostsNotReadyForMultiNetworkNics(Mockito.eq(virtualMachineProfileMock), Mockito.eq(dc), Mockito.any()); + } + @Test(expected = InvalidParameterValueException.class) public void testDuplicateRegistrations() { String accountName = "account"; diff --git a/server/src/test/java/com/cloud/vm/UserVmManagerImplTest.java b/server/src/test/java/com/cloud/vm/UserVmManagerImplTest.java index 4d631b293c17..3d253028c6ad 100644 --- a/server/src/test/java/com/cloud/vm/UserVmManagerImplTest.java +++ b/server/src/test/java/com/cloud/vm/UserVmManagerImplTest.java @@ -137,10 +137,12 @@ import com.cloud.exception.PermissionDeniedException; import com.cloud.exception.ResourceAllocationException; import com.cloud.exception.ResourceUnavailableException; +import com.cloud.host.DetailVO; import com.cloud.host.Host; import com.cloud.host.HostVO; import com.cloud.host.Status; import com.cloud.host.dao.HostDao; +import com.cloud.host.dao.HostDetailsDao; import com.cloud.hypervisor.Hypervisor; import com.cloud.network.Network; import com.cloud.network.NetworkModel; @@ -156,6 +158,8 @@ import com.cloud.network.dao.PhysicalNetworkVO; import com.cloud.network.element.UserDataServiceProvider; import com.cloud.network.guru.NetworkGuru; +import com.cloud.network.router.CommandSetupHelper; +import com.cloud.network.router.NetworkHelper; import com.cloud.network.rules.FirewallRuleVO; import com.cloud.network.rules.PortForwardingRule; import com.cloud.network.rules.dao.PortForwardingRulesDao; @@ -208,7 +212,10 @@ import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.utils.exception.ExceptionProxyObject; import com.cloud.utils.fsm.NoTransitionException; +import com.cloud.vm.dao.DomainRouterDao; import com.cloud.vm.dao.NicDao; +import com.cloud.vm.dao.NicNetworkMapDao; +import com.cloud.vm.dao.NicNetworkMapVO; import com.cloud.vm.dao.UserVmDao; import com.cloud.vm.dao.VMInstanceDetailsDao; import com.cloud.vm.snapshot.VMSnapshotVO; @@ -239,6 +246,18 @@ public class UserVmManagerImplTest { @Mock protected NicDao nicDao; + @Mock + private NicNetworkMapDao nicNetworkMapDao; + + @Mock + private DomainRouterDao routerDao; + + @Mock + private CommandSetupHelper commandSetupHelper; + + @Mock + private NetworkHelper nwHelper; + @Mock private NetworkDao _networkDao; @@ -326,6 +345,9 @@ public class UserVmManagerImplTest { @Mock HostDao hostDao; + @Mock + private HostDetailsDao hostDetailsDao; + @Mock private VolumeVO volumeVOMock; @@ -559,6 +581,110 @@ public void afterTest() { } } + private NicVO mockNicWithId(long id) { + NicVO nic = Mockito.mock(NicVO.class); + Mockito.when(nic.getId()).thenReturn(id); + return nic; + } + + @Test + public void syncNicNetworkAssociationDhcpEntriesIsNoOpForANicWithoutAssociations() { + UserVmVO vm = Mockito.mock(UserVmVO.class); + NicVO nic = mockNicWithId(11L); + + userVmManagerImpl.syncNicNetworkAssociationDhcpEntries(vm, nic); + + Mockito.verify(_networkDao, Mockito.never()).findById(Mockito.anyLong()); + Mockito.verifyNoInteractions(commandSetupHelper); + } + + @Test + public void syncNicNetworkAssociationDhcpEntriesPushesDhcpEntryForAnActiveAssociation() throws Exception { + UserVmVO vm = Mockito.mock(UserVmVO.class); + NicVO nic = mockNicWithId(11L); + NicNetworkMapVO association = new NicNetworkMapVO(11L, 206L, "10.1.1.50", null); + Mockito.when(nicNetworkMapDao.listByNicId(11L)).thenReturn(Collections.singletonList(association)); + NetworkVO associatedNetwork = Mockito.mock(NetworkVO.class); + Mockito.when(_networkDao.findById(206L)).thenReturn(associatedNetwork); + DomainRouterVO router = Mockito.mock(DomainRouterVO.class); + Mockito.when(router.getState()).thenReturn(VirtualMachine.State.Running); + Mockito.when(routerDao.findByNetwork(206L)).thenReturn(Collections.singletonList(router)); + + userVmManagerImpl.syncNicNetworkAssociationDhcpEntries(vm, nic); + + Mockito.verify(commandSetupHelper).createDhcpEntryCommand(Mockito.eq(router), Mockito.eq(vm), Mockito.any(), Mockito.eq("10.1.1.50"), + Mockito.any(), Mockito.any(), Mockito.any(), Mockito.eq(206L), Mockito.eq(false), Mockito.eq(false), Mockito.any()); + Mockito.verify(nwHelper).sendCommandsToRouter(Mockito.eq(router), Mockito.any()); + } + + @Test + public void syncNicNetworkAssociationDhcpEntriesCleansUpARemovedAssociation() throws Exception { + UserVmVO vm = Mockito.mock(UserVmVO.class); + NicVO nic = mockNicWithId(11L); + NicNetworkMapVO removedAssociation = new NicNetworkMapVO(11L, 207L, "10.1.1.60", null); + Mockito.when(nicNetworkMapDao.listRemovedByNicId(11L)).thenReturn(Collections.singletonList(removedAssociation)); + NetworkVO removedNetwork = Mockito.mock(NetworkVO.class); + Mockito.when(_networkDao.findById(207L)).thenReturn(removedNetwork); + DomainRouterVO router = Mockito.mock(DomainRouterVO.class); + Mockito.when(router.getState()).thenReturn(VirtualMachine.State.Running); + Mockito.when(routerDao.findByNetwork(207L)).thenReturn(Collections.singletonList(router)); + + userVmManagerImpl.syncNicNetworkAssociationDhcpEntries(vm, nic); + + Mockito.verify(commandSetupHelper).createDhcpEntryCommand(Mockito.eq(router), Mockito.eq(vm), Mockito.any(), Mockito.eq("10.1.1.60"), + Mockito.any(), Mockito.any(), Mockito.any(), Mockito.eq(207L), Mockito.eq(false), Mockito.eq(true), Mockito.any()); + Mockito.verify(nwHelper).sendCommandsToRouter(Mockito.eq(router), Mockito.any()); + } + + private VMInstanceVO mockVmWithId(long id) { + VMInstanceVO vm = Mockito.mock(VMInstanceVO.class); + Mockito.when(vm.getId()).thenReturn(id); + return vm; + } + + private NicVO mockNicWithMultiNetwork(boolean multiNetwork) { + NicVO nic = Mockito.mock(NicVO.class); + Mockito.when(nic.getMultiNetwork()).thenReturn(multiNetwork); + return nic; + } + + @Test + public void isHostReadyForMultiNetworkNicsIsReadyForAVmWithNoTrunkNic() { + VMInstanceVO vm = mockVmWithId(1L); + NicVO ordinaryNic = mockNicWithMultiNetwork(false); + Mockito.when(nicDao.listByVmId(1L)).thenReturn(Collections.singletonList(ordinaryNic)); + Host host = Mockito.mock(Host.class); + + Assert.assertTrue(userVmManagerImpl.isHostReadyForMultiNetworkNics(vm, host)); + Mockito.verifyNoInteractions(hostDetailsDao); + } + + @Test + public void isHostReadyForMultiNetworkNicsRejectsATrunkNicVmOnAHostWithoutVlanFiltering() { + VMInstanceVO vm = mockVmWithId(1L); + NicVO trunkNic = mockNicWithMultiNetwork(true); + Mockito.when(nicDao.listByVmId(1L)).thenReturn(Collections.singletonList(trunkNic)); + Host host = Mockito.mock(Host.class); + Mockito.when(host.getId()).thenReturn(2L); + Mockito.when(hostDetailsDao.findDetail(2L, Host.HOST_VLAN_FILTERING_ENABLED)).thenReturn(null); + + Assert.assertFalse(userVmManagerImpl.isHostReadyForMultiNetworkNics(vm, host)); + } + + @Test + public void isHostReadyForMultiNetworkNicsAcceptsATrunkNicVmOnAHostWithVlanFiltering() { + VMInstanceVO vm = mockVmWithId(1L); + NicVO trunkNic = mockNicWithMultiNetwork(true); + Mockito.when(nicDao.listByVmId(1L)).thenReturn(Collections.singletonList(trunkNic)); + Host host = Mockito.mock(Host.class); + Mockito.when(host.getId()).thenReturn(2L); + DetailVO detail = Mockito.mock(DetailVO.class); + Mockito.when(detail.getValue()).thenReturn("true"); + Mockito.when(hostDetailsDao.findDetail(2L, Host.HOST_VLAN_FILTERING_ENABLED)).thenReturn(detail); + + Assert.assertTrue(userVmManagerImpl.isHostReadyForMultiNetworkNics(vm, host)); + } + @Test public void validateGuestOsIdForUpdateVirtualMachineCommandTestOsTypeNull() { Mockito.when(updateVmCommand.getOsTypeId()).thenReturn(null); diff --git a/server/src/test/java/com/cloud/vpc/MockNetworkManagerImpl.java b/server/src/test/java/com/cloud/vpc/MockNetworkManagerImpl.java index 1239dd23e7ec..0a5a58613363 100644 --- a/server/src/test/java/com/cloud/vpc/MockNetworkManagerImpl.java +++ b/server/src/test/java/com/cloud/vpc/MockNetworkManagerImpl.java @@ -26,6 +26,9 @@ import org.apache.cloudstack.acl.ControlledEntity.ACLType; import org.apache.cloudstack.api.command.admin.address.ReleasePodIpCmdByAdmin; +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.admin.network.DedicateGuestVlanRangeCmd; import org.apache.cloudstack.api.command.admin.network.ListDedicatedGuestVlanRangesCmd; import org.apache.cloudstack.api.command.admin.network.ListGuestVlansCmd; @@ -1065,6 +1068,25 @@ public List listVmNicSecondaryIps(ListNicsCmd listNics return null; } + @Override + public Nic associateNetworkToNic(AssociateNetworkToNicCmd cmd) { + return null; + } + + @Override + public Nic disassociateNetworkFromNic(DisassociateNetworkFromNicCmd cmd) { + return null; + } + + @Override + public Nic changeNicPrimaryNetwork(ChangeNicPrimaryNetworkCmd cmd) { + return null; + } + + @Override + public void associateNetworksToNic(Nic nic, List networkIds, Map requestedIps) { + } + @Override public boolean releasePodIp(ReleasePodIpCmdByAdmin ip) throws CloudRuntimeException { return true; diff --git a/server/src/test/java/com/cloud/vpc/MockNetworkModelImpl.java b/server/src/test/java/com/cloud/vpc/MockNetworkModelImpl.java index 83f3fda383c8..f24836c09e00 100644 --- a/server/src/test/java/com/cloud/vpc/MockNetworkModelImpl.java +++ b/server/src/test/java/com/cloud/vpc/MockNetworkModelImpl.java @@ -195,6 +195,12 @@ public Nic getNicInNetwork(long vmId, long networkId) { return null; } + @Override + public Pair getNicAndIpInNetwork(long vmId, long networkId) { + // TODO Auto-generated method stub + return null; + } + /* (non-Javadoc) * @see com.cloud.network.NetworkModel#getNicsForTraffic(long, com.cloud.network.Networks.TrafficType) */ diff --git a/server/src/test/java/org/apache/cloudstack/storage/template/VnfTemplateManagerImplTest.java b/server/src/test/java/org/apache/cloudstack/storage/template/VnfTemplateManagerImplTest.java index b9565ebb2922..fe64bc4e8a4f 100644 --- a/server/src/test/java/org/apache/cloudstack/storage/template/VnfTemplateManagerImplTest.java +++ b/server/src/test/java/org/apache/cloudstack/storage/template/VnfTemplateManagerImplTest.java @@ -249,6 +249,26 @@ public void testValidateVnfApplianceNicsWithMissingNetworkId() { vnfTemplateManagerImpl.validateVnfApplianceNics(template, networkIds, null); } + @Test(expected = InvalidParameterValueException.class) + public void testValidateVnfApplianceTrunkNicRejectsManagementDevice() { + when(template.getId()).thenReturn(templateId); + VnfTemplateNicVO vnfNic1 = new VnfTemplateNicVO(templateId, 0L, "eth0", true, true, "first"); + VnfTemplateNicVO vnfNic2 = new VnfTemplateNicVO(templateId, 1L, "eth1", true, false, "second"); + Mockito.doReturn(Arrays.asList(vnfNic1, vnfNic2)).when(vnfTemplateNicDao).listByTemplateId(templateId); + + vnfTemplateManagerImpl.validateVnfApplianceTrunkNic(template, 0L); + } + + @Test + public void testValidateVnfApplianceTrunkNicAllowsDataPlaneDevice() { + when(template.getId()).thenReturn(templateId); + VnfTemplateNicVO vnfNic1 = new VnfTemplateNicVO(templateId, 0L, "eth0", true, true, "first"); + VnfTemplateNicVO vnfNic2 = new VnfTemplateNicVO(templateId, 1L, "eth1", true, false, "second"); + Mockito.doReturn(Arrays.asList(vnfNic1, vnfNic2)).when(vnfTemplateNicDao).listByTemplateId(templateId); + + vnfTemplateManagerImpl.validateVnfApplianceTrunkNic(template, 1L); + } + @Test public void testGetManagementNetworkAndIp() { when(template.getId()).thenReturn(templateId); diff --git a/systemvm/debian/opt/cloud/bin/cs_dhcp.py b/systemvm/debian/opt/cloud/bin/cs_dhcp.py index cd6574fd9265..cfebd1254388 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 diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json index 99bf2cf7aef9..b4470132c476 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -443,9 +443,11 @@ "label.assign.instance.another": "Assign Instance to another Account", "label.assign.vms": "Assign Instances", "label.assigning.vms": "Assigning Instances", +"label.associate.network": "Associate Network", "label.associatednetwork": "Associated Network", "label.associatednetworkid": "Associated Network ID", "label.associatednetworkname": "Network name", +"label.associatednetworks": "Associated Networks (trunk)", "label.asyncbackup": "Async backup", "label.attach.vol.to.instance": "Attach the created Volume to an existing Instance", "label.attaching": "Attaching", @@ -937,6 +939,7 @@ "label.disable.vpn": "Disable remote access VPN", "label.disable.webhook": "Disable Webhook", "label.disabled": "Disabled", +"label.disassociate.network": "Disassociate Network", "label.disconnected": "Last disconnected", "label.discover.gpu.devices": "Discover GPU devices", "label.disk": "Disk", @@ -1317,6 +1320,7 @@ "label.host.vddk.support": "VDDK Support", "label.host.vddk.version": "VDDK Version", "label.host.tag": "Host tag", +"label.host.vlanfilteringenabled": "VLAN filtering", "label.host.virtv2v.version": "Virt-v2v Version", "label.hostcontrolstate": "Compute Resource Status", "label.hostid": "Host", @@ -1767,6 +1771,8 @@ "label.move.to.bottom": "Move to bottom", "label.move.to.top": "Move to top", "label.move.up.row": "Move up one row", +"label.multinetworknic.hosts.ready": "Multi-VLAN trunk NIC ready hosts", +"label.multinetworknic.hosts.ready.count": "{ready} of {total} hosts ready", "label.my.isos": "My ISOs", "label.my.templates": "My Templates", "label.na": "N/A", @@ -1794,6 +1800,7 @@ "label.network": "Network", "label.network.acl": "Network ACL", "label.network.acls": "Network ACLs", +"label.network.add.associated": "Add Associated Networks", "label.network.addvm": "Add Network to Instance", "label.network.desc": "Network desc", "label.network.domain": "Network Domain", @@ -1812,6 +1819,8 @@ "label.networkdevicetype": "Type", "label.networkdomain": "Network domain", "label.networkid": "Network", +"label.nic.associatednetworks": "Associated Networks", +"label.nic.trunked": "Trunked", "label.networkkbsread": "Network read", "label.networkkbswrite": "Network write", "label.networklimit": "Network limits", @@ -2895,8 +2904,10 @@ "label.vlan": "VLAN/VNI", "label.vlan.range": "VLAN/VNI range", "label.vlan.vni.ranges": "VLAN/VNI range(s)", +"label.vlanfilteringenabled": "VLAN filtering enabled", "label.vlanid": "VLAN/VNI ID", "label.vlanrange": "VLAN/VNI range", +"label.vlantrunkxmlsupported": "VLAN trunk XML supported", "label.vm": "Instance", "label.vm.add": "Add Instance", "label.vm.password": "Password of the Instance is", @@ -3321,6 +3332,9 @@ "message.assign.instance.another": "Please specify the Account type, domain, Account name and Network (optional) of the new Account.
If the default NIC of the Instance is on a shared Network, CloudStack will check if the Network can be used by the new Account if you do not specify one Network.
If the default NIC of the Instance is on a isolated Network, and the new Account has more one isolated Networks, you should specify one.", "message.assign.vm.failed": "Failed to assign Instance", "message.assign.vm.processing": "Assigning Instance...", +"message.associate.network.desc": "Select one or more additional Networks to associate with this NIC, delivering them as a multi-VLAN trunk. Requires VLAN-filtering support on the host(s) the Instance may run on.", +"message.associate.network.loop.warning": "Instances attached to this NIC will receive multiple L2 segments. Misconfiguration inside guest VMs may cause L2 loops. Ensure proper VLAN handling.", +"message.associate.network.processing": "Associating Network(s)...", "message.attach.volume": "Please fill in the following data to attach a new volume. If you are attaching a disk volume to a Windows based Instance, you will need to reboot the Instance to see the attached disk.", "message.attach.volume.failed": "Failed to attach volume.", "message.attach.volume.progress": "Attaching volume", @@ -3560,6 +3574,8 @@ "message.disable.vpn.failed": "Failed to disable VPN.", "message.disable.vpn.processing": "Disabling VPN...", "message.disable.webhook.ssl.verification": "Disabling SSL verification is not recommended", +"message.disassociate.network.confirm": "Please confirm you want to remove this Network association from the NIC.", +"message.disassociate.network.processing": "Disassociating Network...", "message.discovering.feature": "Discovering features, please wait...", "message.disk.offering.created": "Disk offering created:", "message.success.clone.backup.offering": "Successfully cloned backup offering", @@ -3593,6 +3609,8 @@ "message.error.access.key": "Please enter access key.", "message.error.account.delete.name.mismatch": "Name entered doesn't match the account name.", "message.error.add.guest.network": "Either IPv4 fields or IPv6 fields need to be filled when adding a guest Network.", +"message.error.associate.network": "Failed to associate Network(s) with NIC", +"message.error.disassociate.network": "Failed to disassociate Network from NIC", "message.error.add.interface.static.route": "Adding interface Static Route failed", "message.error.add.logical.router": "Adding Logical Router failed", "message.error.add.network.static.route": "Adding Network Static Route failed", @@ -3772,6 +3790,7 @@ "message.host.dedicated": "Host Dedicated", "message.host.dedication.released": "Host dedication released.", "message.host.external.datadisk": "Usage of data disks for the selected template is not applicable", +"message.host.vlanfilteringenabled.tooltip": "VLAN filtering is enabled on this host's guest bridge, so it can accept multi-VLAN trunk NICs", "message.import.running.instance.warning": "The selected VM is powered-on on the VMware Datacenter. The recommended state to convert a VMware VM into KVM is powered-off after a graceful shutdown of the guest OS.", "message.import.vm.tasks": "Import from VMware to KVM tasks", "message.import.volume": "Please specify the domain, account or project name.
If not set, the volume will be imported for the caller.", @@ -3898,6 +3917,7 @@ "message.network.restart.required": "Restart is required for network(s). Click here to view network(s) which require restart.", "message.network.secondaryip": "Please confirm that you would like to acquire a new secondary IP for this NIC. \n NOTE: You need to manually configure the newly-acquired secondary IP inside the virtual machine.", "message.network.selection": "Choose one or more Networks to attach the Instance to.", +"message.nic.trunked.tooltip": "This NIC is a multi-VLAN trunk: in addition to its primary Network, it carries traffic for the associated Networks listed below.", "message.network.selection.new.network": "A new Network can also be created here.", "message.network.updateip": "Please confirm that you would like to change the IP address for this NIC on the Instance.", "message.network.update.nic": "Please confirm that you would like to update this NIC.", @@ -4065,6 +4085,8 @@ "message.success.add.kuberversion": "Successfully added Kubernetes version", "message.success.add.logical.router": "Successfully added Logical Router", "message.success.add.network": "Successfully added Network", +"message.success.associate.network": "Successfully associated Network(s) with NIC", +"message.success.disassociate.network": "Successfully disassociated Network from NIC", "message.success.add.network.acl": "Successfully added Network ACL", "message.success.add.network.static.route": "Successfully added Network Static Route", "message.success.add.network.permissions": "Successfully added Network permissions", diff --git a/ui/src/components/view/ListView.vue b/ui/src/components/view/ListView.vue index 9a7d874fef3e..cc3a98de37ac 100644 --- a/ui/src/components/view/ListView.vue +++ b/ui/src/components/view/ListView.vue @@ -178,6 +178,12 @@ + +   + + {{ $t('label.host.vlanfilteringenabled') }} + + { this.resourceIdToValidLinksMap[record.id] = validateLinks(this.$router, false, record) + if (this.$route.path.startsWith('/host') && record.vlanfilteringenabled && record.zoneid) { + fetchMultiNetworkNicEnabledForZone(record.zoneid) + } }) } } diff --git a/ui/src/config/section/infra/hosts.js b/ui/src/config/section/infra/hosts.js index 046f120dd37a..74727d7f8ae8 100644 --- a/ui/src/config/section/infra/hosts.js +++ b/ui/src/config/section/infra/hosts.js @@ -45,7 +45,7 @@ export default { fields.push('managementservername') return fields }, - details: ['name', 'id', 'resourcestate', 'ipaddress', 'hypervisor', 'externalprovisioner', 'arch', 'type', 'clustername', 'podname', 'zonename', 'lastpinged', 'storageaccessgroups', 'clusterstorageaccessgroups', 'podstorageaccessgroups', 'zonestorageaccessgroups', 'managementservername', 'disconnected', 'created', 'externaldetails'], + details: ['name', 'id', 'resourcestate', 'ipaddress', 'hypervisor', 'externalprovisioner', 'arch', 'type', 'clustername', 'podname', 'zonename', 'vlanfilteringenabled', 'vlantrunkxmlsupported', 'lastpinged', 'storageaccessgroups', 'clusterstorageaccessgroups', 'podstorageaccessgroups', 'zonestorageaccessgroups', 'managementservername', 'disconnected', 'created', 'externaldetails'], tabs: [{ name: 'details', component: shallowRef(defineAsyncComponent(() => import('@/components/view/DetailsTab.vue'))) diff --git a/ui/src/utils/multiNetworkNic.js b/ui/src/utils/multiNetworkNic.js new file mode 100644 index 000000000000..7997ac307118 --- /dev/null +++ b/ui/src/utils/multiNetworkNic.js @@ -0,0 +1,89 @@ +// 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. + +// Shared, module-level cache of the effective (zone-override-or-global) value of the +// multi.network.nic.enabled ConfigKey, keyed by zoneid. Reused by any view that needs to +// gate multi-VLAN trunk NIC UI on whether the feature is actually usable in a given zone +// (e.g. the host list's VLAN-filtering badge, the deploy wizard's associate-network control). +import { reactive } from 'vue' +import { getAPI } from '@/api' + +export const multiNetworkNicEnabledZones = reactive({}) + +// in-flight requests only, keyed by zoneid - dedupes simultaneous callers (e.g. many host rows +// in the same zone on one page) without skipping a fresh re-check on the next page visit +const pendingRequests = {} + +export function isMultiNetworkNicEnabledForZone (zoneid) { + return !!multiNetworkNicEnabledZones[zoneid] +} + +export function fetchMultiNetworkNicEnabledForZone (zoneid) { + if (!zoneid || pendingRequests[zoneid]) { + return + } + pendingRequests[zoneid] = getAPI('listConfigurations', { name: 'multi.network.nic.enabled', zoneid }).then(json => { + const config = json?.listconfigurationsresponse?.configuration?.[0] + multiNetworkNicEnabledZones[zoneid] = !!(config && config.value === 'true') + }).catch(() => {}).finally(() => { + delete pendingRequests[zoneid] + }) +} + +// Flattens a nic's selectable IP targets for rule-creation pickers (Static NAT / Port Forwarding / Load +// Balancing), distinguishing three kinds that are otherwise easy to conflate in the UI: +// - primary: the nic's own network (nic.networkid / nic.ipaddress) +// - secondary: another IP on that SAME network (nic.secondaryip[], pre-existing nic_secondary_ips feature) +// - associated: an IP on a DIFFERENT network this nic trunks to (nic.associatednetworks[], nic_network_map) +// Each option carries the networkid a rule-creation call (enableStaticNat/createPortForwardingRule/ +// assignToLoadBalancerRule) must submit alongside its ip - an associated option's networkid differs from +// the nic's own, which is the one real behavioral difference from a secondary IP. +export function buildNicIpOptions (nic) { + const options = [] + if (nic.ipaddress) { + options.push({ kind: 'primary', ip: nic.ipaddress, networkid: nic.networkid, networkname: nic.networkname, nicid: nic.id }) + } + for (const secondary of (nic.secondaryip || [])) { + options.push({ kind: 'secondary', ip: secondary.ipaddress, networkid: nic.networkid, networkname: nic.networkname, nicid: nic.id }) + } + for (const association of (nic.associatednetworks || [])) { + options.push({ kind: 'associated', ip: association.ipaddress, networkid: association.networkid, networkname: association.networkname, nicid: nic.id }) + } + return options +} + +// Groups buildNicIpOptions() output into labeled sections for a picker (e.g. ), +// in a fixed, meaningful order - primary first, then secondary, then associated - omitting any section +// that has nothing to show (the common legacy case has only a primary section). +export function groupNicIpOptionsByKind (options, translate) { + return [ + { kind: 'primary', label: translate('label.primary') }, + { kind: 'secondary', label: translate('label.secondaryips') }, + { kind: 'associated', label: translate('label.associatednetworks') } + ] + .map(({ kind, label }) => ({ kind, label, items: options.filter(item => item.kind === kind) })) + .filter(group => group.items.length > 0) +} + +// Resolves the networkid a rule-creation call (enableStaticNat/createPortForwardingRule/ +// assignToLoadBalancerRule) must submit for a chosen ip, falling back to fallbackNetworkId when ip +// doesn't match any option (e.g. options haven't loaded yet) - keeps existing callers' behavior for +// every pre-existing case, only changing the outcome for an associated-network selection. +export function resolveNetworkIdForIp (options, ip, fallbackNetworkId) { + const match = (options || []).find(item => item.ip === ip) + return match ? match.networkid : fallbackNetworkId +} diff --git a/ui/src/views/compute/DeployVM.vue b/ui/src/views/compute/DeployVM.vue index 4fa5c14d86e3..a6c5f815db96 100644 --- a/ui/src/views/compute/DeployVM.vue +++ b/ui/src/views/compute/DeployVM.vue @@ -448,9 +448,12 @@ v-if="networks.length > 0" :items="networks" :preFillContent="dataPreFill" + :canTrunkNics="canTrunkNics" + :zoneId="zoneId" @update-network-config="($event) => updateNetworkConfig($event)" @handler-error="($event) => hasError = $event" @select-default-network-item="($event) => updateDefaultNetworks($event)" + @update-trunk-groups="($event) => updateTrunkGroups($event)" > @@ -1105,6 +1108,8 @@ export default { diskOffering: {}, affinityGroups: [], networks: [], + // networks seen across all pages, keyed by id - options.networks only holds the current page + networksCache: {}, networksAdd: [], zone: {}, sshKeyPairs: [], @@ -1159,6 +1164,7 @@ export default { selectedZone: '', formModel: {}, nicToNetworkSelection: [], + trunkGroups: {}, selectedArchitecture: null, isLeaseFeatureEnabled: this.$store.getters.features.instanceleaseenabled, showLeaseOptions: false, @@ -1185,6 +1191,9 @@ export default { rootDiskSize () { return this.showRootDiskSizeChanger && this.rootDiskSizeFixed > 0 }, + canTrunkNics () { + return this.hypervisor === 'KVM' && ('associateNetworkToNic' in this.$store.getters.apis) + }, isNormalAndDomainUser () { return ['DomainAdmin', 'User'].includes(this.$store.getters.userInfo.roletype) }, @@ -1626,7 +1635,7 @@ export default { this.zone = _.find(this.options.zones, (option) => option.id === this.instanceConfig.zoneid) this.affinityGroups = _.filter(this.options.affinityGroups, (option) => _.includes(instanceConfig.affinitygroupids, option.id)) - this.networks = this.getSelectedNetworksWithExistingConfig(_.filter(this.options.networks, (option) => _.includes(instanceConfig.networkids, option.id))) + this.networks = this.getSelectedNetworksWithExistingConfig(_.filter(Object.values(this.networksCache), (option) => _.includes(instanceConfig.networkids, option.id))) this.diskOffering = _.find(this.options.diskOfferings, (option) => option.id === instanceConfig.diskofferingid) this.sshKeyPair = _.find(this.options.sshKeyPairs, (option) => option.name === instanceConfig.keypair) @@ -2240,6 +2249,9 @@ export default { updateNetworkConfig (networks) { this.networkConfig = networks }, + updateTrunkGroups (trunkGroups) { + this.trunkGroups = trunkGroups + }, updateSshKeyPairs (names) { this.form.keypairs = names this.sshKeyPairs = names.map((sshKeyPair) => { return sshKeyPair.name }) @@ -2524,9 +2536,48 @@ export default { deployVmData['nicnetworklist[' + j + '].network'] = nicNetwork.network } } else { - const arrNetwork = [] networkIds = values.networkids - if (networkIds.length > 0) { + if (!networkIds || networkIds.length === 0) { + this.$notification.error({ + message: this.$t('message.request.failed'), + description: this.$t('message.step.4.continue') + }) + this.loading.deploy = false + return + } + const groupedAwayNetworkIds = Object.values(this.trunkGroups || {}).flat() + const hasTrunkGroups = groupedAwayNetworkIds.length > 0 + if (hasTrunkGroups) { + // one or more nics group several networks onto a single nic (multi-VLAN trunk) - mutually + // exclusive with iptonetworklist at the API level, so every nic (grouped or not) goes + // through nicnetworkslist here instead. + const ownNicNetworkIds = networkIds.filter(id => !groupedAwayNetworkIds.includes(id)) + const orderedNicNetworkIds = [] + for (const id of ownNicNetworkIds) { + if (id === this.defaultnetworkid) { + orderedNicNetworkIds.unshift(id) + } else { + orderedNicNetworkIds.push(id) + } + } + for (let j = 0; j < orderedNicNetworkIds.length; j++) { + const primaryNetworkId = orderedNicNetworkIds[j] + const groupNetworkIds = [primaryNetworkId, ...(this.trunkGroups[primaryNetworkId] || [])] + deployVmData['nicnetworkslist[' + j + '].networkids'] = groupNetworkIds.join(',') + if (this.networkConfig.length > 0) { + const networkConfig = this.networkConfig.filter((item) => item.key === primaryNetworkId) + if (networkConfig && networkConfig.length > 0) { + if (networkConfig[0].ipAddress) { + deployVmData['nicnetworkslist[' + j + '].ip4address'] = networkConfig[0].ipAddress + } + if (networkConfig[0].macAddress) { + deployVmData['nicnetworkslist[' + j + '].macaddress'] = networkConfig[0].macAddress + } + } + } + } + } else { + const arrNetwork = [] for (let i = 0; i < networkIds.length; i++) { if (networkIds[i] === this.defaultnetworkid) { const ipToNetwork = { @@ -2540,21 +2591,14 @@ export default { arrNetwork.push(ipToNetwork) } } - } else { - this.$notification.error({ - message: this.$t('message.request.failed'), - description: this.$t('message.step.4.continue') - }) - this.loading.deploy = false - return - } - for (let j = 0; j < arrNetwork.length; j++) { - deployVmData['iptonetworklist[' + j + '].networkid'] = arrNetwork[j].networkid - if (this.networkConfig.length > 0) { - const networkConfig = this.networkConfig.filter((item) => item.key === arrNetwork[j].networkid) - if (networkConfig && networkConfig.length > 0) { - deployVmData['iptonetworklist[' + j + '].ip'] = networkConfig[0].ipAddress ? networkConfig[0].ipAddress : undefined - deployVmData['iptonetworklist[' + j + '].mac'] = networkConfig[0].macAddress ? networkConfig[0].macAddress : undefined + for (let j = 0; j < arrNetwork.length; j++) { + deployVmData['iptonetworklist[' + j + '].networkid'] = arrNetwork[j].networkid + if (this.networkConfig.length > 0) { + const networkConfig = this.networkConfig.filter((item) => item.key === arrNetwork[j].networkid) + if (networkConfig && networkConfig.length > 0) { + deployVmData['iptonetworklist[' + j + '].ip'] = networkConfig[0].ipAddress ? networkConfig[0].ipAddress : undefined + deployVmData['iptonetworklist[' + j + '].mac'] = networkConfig[0].macAddress ? networkConfig[0].macAddress : undefined + } } } } @@ -2789,6 +2833,12 @@ export default { param.opts = response this.options[name] = response + if (name === 'networks') { + for (const network of response) { + this.networksCache[network.id] = network + } + } + if (name === 'hypervisors') { const hypervisorFromResponse = response[0] && response[0].name ? response[0].name : null this.dataPreFill.hypervisor = hypervisorFromResponse diff --git a/ui/src/views/compute/DeployVnfAppliance.vue b/ui/src/views/compute/DeployVnfAppliance.vue index 05f804ef4eac..4c6882671931 100644 --- a/ui/src/views/compute/DeployVnfAppliance.vue +++ b/ui/src/views/compute/DeployVnfAppliance.vue @@ -362,7 +362,10 @@ :items="templateVnfNics" :templateNics="templateNics" :networks="networks" - @update-vnf-nic-networks="($event) => updateVnfNicNetworks($event)" /> + :canTrunkNics="canTrunkNics" + :zoneId="zoneId" + @update-vnf-nic-networks="($event) => updateVnfNicNetworks($event)" + @update-vnf-trunk-groups="($event) => updateVnfTrunkGroups($event)" />
@@ -985,6 +988,7 @@ export default { templateProperties: {}, templateVnfNics: [], vnfNicNetworks: {}, + vnfTrunkGroups: {}, selectedTemplateConfiguration: {}, iso: {}, hypervisor: '', @@ -992,6 +996,8 @@ export default { diskOffering: {}, affinityGroups: [], networks: [], + // networks seen across all pages, keyed by id - options.networks only holds the current page + networksCache: {}, networksAdd: [], zone: {}, sshKeyPairs: [], @@ -1055,6 +1061,9 @@ export default { rootDiskSize () { return this.showRootDiskSizeChanger && this.rootDiskSizeFixed > 0 }, + canTrunkNics () { + return this.hypervisor === 'KVM' && ('associateNetworkToNic' in this.$store.getters.apis) + }, isNormalAndDomainUser () { return ['DomainAdmin', 'User'].includes(this.$store.getters.userInfo.roletype) }, @@ -1434,7 +1443,7 @@ export default { } this.zone = _.find(this.options.zones, (option) => option.id === vnfAppConfig.zoneid) this.affinityGroups = _.filter(this.options.affinityGroups, (option) => _.includes(vnfAppConfig.affinitygroupids, option.id)) - this.networks = this.getSelectedNetworksWithExistingConfig(_.filter(this.options.networks, (option) => _.includes(vnfAppConfig.networkids, option.id))) + this.networks = this.getSelectedNetworksWithExistingConfig(_.filter(Object.values(this.networksCache), (option) => _.includes(vnfAppConfig.networkids, option.id))) this.diskOffering = _.find(this.options.diskOfferings, (option) => option.id === vnfAppConfig.diskofferingid) this.sshKeyPair = _.find(this.options.sshKeyPairs, (option) => option.name === vnfAppConfig.keypair) @@ -1944,6 +1953,9 @@ export default { updateVnfNicNetworks (vnfNicNetworks) { this.vnfNicNetworks = vnfNicNetworks || {} }, + updateVnfTrunkGroups (vnfTrunkGroups) { + this.vnfTrunkGroups = vnfTrunkGroups || {} + }, updateSshKeyPairs (names) { this.form.keypairs = names this.sshKeyPairs = names.map((sshKeyPair) => { return sshKeyPair.name }) @@ -2264,9 +2276,32 @@ export default { networkIds = values.networkids if (networkIds.length > 0) { if (this.templateVnfNics && this.templateVnfNics.length > 0) { + const hasVnfTrunkGroups = this.vnfTrunkGroups && Object.values(this.vnfTrunkGroups).some(ids => ids && ids.length > 0) + let k = 0 for (const templateVnfNic of this.templateVnfNics) { const vnfNicNetworkId = this.vnfNicNetworks[String(templateVnfNic.deviceid)]?.id || null - if (vnfNicNetworkId) { + if (!vnfNicNetworkId) { + continue + } + if (hasVnfTrunkGroups) { + // one or more VNF nics group several networks onto a single nic (multi-VLAN trunk) - + // mutually exclusive with iptonetworklist at the API level, so every nic (grouped or + // not) goes through nicnetworkslist here instead. + const assocIds = this.vnfTrunkGroups[String(templateVnfNic.deviceid)] || [] + createVnfAppData['nicnetworkslist[' + k + '].networkids'] = [vnfNicNetworkId, ...assocIds].join(',') + if (this.networkConfig.length > 0) { + const networkConfig = this.networkConfig.filter((item) => item.key === vnfNicNetworkId) + if (networkConfig && networkConfig.length > 0) { + if (networkConfig[0].ipAddress) { + createVnfAppData['nicnetworkslist[' + k + '].ip4address'] = networkConfig[0].ipAddress + } + if (networkConfig[0].macAddress) { + createVnfAppData['nicnetworkslist[' + k + '].macaddress'] = networkConfig[0].macAddress + } + } + } + k++ + } else { const ipToNetwork = { networkid: vnfNicNetworkId } @@ -2513,6 +2548,12 @@ export default { param.opts = response this.options[name] = response + if (name === 'networks') { + for (const network of response) { + this.networksCache[network.id] = network + } + } + if (name === 'hypervisors') { const hypervisorFromResponse = response[0] && response[0].name ? response[0].name : null this.dataPreFill.hypervisor = hypervisorFromResponse diff --git a/ui/src/views/compute/MigrateWizard.vue b/ui/src/views/compute/MigrateWizard.vue index 0e8c08d320fd..b24c44618342 100644 --- a/ui/src/views/compute/MigrateWizard.vue +++ b/ui/src/views/compute/MigrateWizard.vue @@ -49,6 +49,11 @@ class="host-item__suitability-icon" twoToneColor="#52c41a" v-if="record.suitableformigration" /> + + +
{{ text }}
{{ $t('label.cidr') + ': ' + record.cidr }} +
+ + {{ networkNameById(assocId) }} + +
+ +