Add API to enable/disable NICs for KVM (#12819)

This commit is contained in:
Henrique Sato 2026-03-31 05:14:20 -03:00 committed by GitHub
parent 18075ae4a9
commit 7eea9ed448
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
35 changed files with 693 additions and 4 deletions

View File

@ -33,6 +33,7 @@ public class NicTO extends NetworkTO {
boolean dpdkEnabled;
Integer mtu;
Long networkId;
boolean enabled;
String networkSegmentName;
@ -154,4 +155,12 @@ public class NicTO extends NetworkTO {
public void setNetworkSegmentName(String networkSegmentName) {
this.networkSegmentName = networkSegmentName;
}
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
}

View File

@ -162,4 +162,6 @@ public interface Nic extends Identity, InternalIdentity {
String getIPv6Address();
Integer getMtu();
boolean isEnabled();
}

View File

@ -52,6 +52,7 @@ public class NicProfile implements InternalIdentity, Serializable {
boolean defaultNic;
Integer networkRate;
boolean isSecurityGroupEnabled;
boolean enabled;
Integer orderIndex;
@ -87,6 +88,7 @@ public class NicProfile implements InternalIdentity, Serializable {
broadcastType = network.getBroadcastDomainType();
trafficType = network.getTrafficType();
format = nic.getAddressFormat();
enabled = nic.isEnabled();
iPv4Address = nic.getIPv4Address();
iPv4Netmask = nic.getIPv4Netmask();
@ -414,6 +416,14 @@ public class NicProfile implements InternalIdentity, Serializable {
this.ipv4AllocationRaceCheck = ipv4AllocationRaceCheck;
}
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
//
// OTHER METHODS
//

View File

@ -40,6 +40,7 @@ import org.apache.cloudstack.api.command.user.vm.ScaleVMCmd;
import org.apache.cloudstack.api.command.user.vm.StartVMCmd;
import org.apache.cloudstack.api.command.user.vm.UpdateDefaultNicForVMCmd;
import org.apache.cloudstack.api.command.user.vm.UpdateVMCmd;
import org.apache.cloudstack.api.command.user.vm.UpdateVmNicCmd;
import org.apache.cloudstack.api.command.user.vm.UpdateVmNicIpCmd;
import org.apache.cloudstack.api.command.user.vm.UpgradeVMCmd;
import org.apache.cloudstack.api.command.user.vmgroup.CreateVMGroupCmd;
@ -152,6 +153,8 @@ public interface UserVmService {
*/
UserVm updateNicIpForVirtualMachine(UpdateVmNicIpCmd cmd);
UserVm updateVirtualMachineNic(UpdateVmNicCmd cmd);
UserVm recoverVirtualMachine(RecoverVMCmd cmd) throws ResourceAllocationException;
/**

View File

@ -343,6 +343,8 @@ public interface ResponseGenerator {
UserVm findUserVmById(Long vmId);
UserVm findUserVmByNicId(Long nicId);
Volume findVolumeById(Long volumeId);
Account findAccountByNameDomain(String accountName, Long domainId);

View File

@ -0,0 +1,95 @@
// 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.user.vm;
import org.apache.cloudstack.acl.RoleType;
import org.apache.cloudstack.api.ACL;
import org.apache.cloudstack.api.APICommand;
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.ResponseObject;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.response.NicResponse;
import org.apache.cloudstack.api.response.UserVmResponse;
import org.apache.cloudstack.context.CallContext;
import com.cloud.event.EventTypes;
import com.cloud.user.Account;
import com.cloud.uservm.UserVm;
import java.util.ArrayList;
import java.util.EnumSet;
@APICommand(name = "updateVmNic", description = "Updates the specified VM NIC", responseObject = NicResponse.class, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false,
authorized = { RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User })
public class UpdateVmNicCmd extends BaseAsyncCmd {
@ACL
@Parameter(name = ApiConstants.NIC_ID, type = CommandType.UUID, entityType = NicResponse.class, required = true, description = "NIC ID")
private Long nicId;
@Parameter(name = ApiConstants.ENABLED, type = CommandType.BOOLEAN, description = "If true, sets the NIC state to UP; otherwise, sets the NIC state to DOWN")
private Boolean enabled;
public Long getNicId() {
return nicId;
}
public Boolean isEnabled() {
return enabled;
}
@Override
public String getEventType() {
return EventTypes.EVENT_NIC_UPDATE;
}
@Override
public String getEventDescription() {
return String.format("Updating NIC %s.", getResourceUuid(ApiConstants.NIC_ID));
}
@Override
public long getEntityOwnerId() {
UserVm vm = _responseGenerator.findUserVmByNicId(nicId);
if (vm == null) {
return Account.ACCOUNT_ID_SYSTEM;
}
return vm.getAccountId();
}
@Override
public void execute() {
CallContext.current().setEventDetails(String.format("NIC ID: %s", getResourceUuid(ApiConstants.NIC_ID)));
UserVm result = _userVmService.updateVirtualMachineNic(this);
ArrayList<ApiConstants.VMDetails> dc = new ArrayList<>();
dc.add(ApiConstants.VMDetails.valueOf("nics"));
EnumSet<ApiConstants.VMDetails> details = EnumSet.copyOf(dc);
if (result != null){
UserVmResponse response = _responseGenerator.createUserVmResponse(ResponseObject.ResponseView.Restricted, "virtualmachine", details, result).get(0);
response.setResponseName(getCommandName());
this.setResponseObject(response);
} else {
throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to update NIC from VM.");
}
}
}

View File

@ -146,6 +146,10 @@ public class NicResponse extends BaseResponse {
@Param(description = "Public IP address associated with this NIC via Static NAT rule")
private String publicIp;
@SerializedName(ApiConstants.ENABLED)
@Param(description = "whether the NIC is enabled or not")
private Boolean isEnabled;
public void setVmId(String vmId) {
this.vmId = vmId;
}
@ -416,4 +420,12 @@ public class NicResponse extends BaseResponse {
public void setPublicIp(String publicIp) {
this.publicIp = publicIp;
}
public Boolean getEnabled() {
return isEnabled;
}
public void setEnabled(Boolean enabled) {
isEnabled = enabled;
}
}

View File

@ -0,0 +1,27 @@
// 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 UpdateVmNicAnswer extends Answer {
public UpdateVmNicAnswer() {
}
public UpdateVmNicAnswer(UpdateVmNicCommand cmd, boolean success, String result) {
super(cmd, success, result);
}
}

View File

@ -0,0 +1,51 @@
// 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 UpdateVmNicCommand extends Command {
String nicMacAddress;
String instanceName;
Boolean enabled;
@Override
public boolean executeInSequence() {
return true;
}
protected UpdateVmNicCommand() {
}
public UpdateVmNicCommand(String nicMacAdderss, String instanceName, Boolean enabled) {
this.nicMacAddress = nicMacAdderss;
this.instanceName = instanceName;
this.enabled = enabled;
}
public String getNicMacAddress() {
return nicMacAddress;
}
public String getVmName() {
return instanceName;
}
public Boolean isEnabled() {
return enabled;
}
}

View File

@ -230,6 +230,8 @@ public interface VirtualMachineManager extends Manager {
Boolean updateDefaultNicForVM(VirtualMachine vm, Nic nic, Nic defaultNic);
boolean updateVmNic(VirtualMachine vm, Nic nic, Boolean enabled) throws ResourceUnavailableException;
/**
* @param vm
* @param network

View File

@ -153,6 +153,8 @@ import com.cloud.agent.api.UnPlugNicAnswer;
import com.cloud.agent.api.UnPlugNicCommand;
import com.cloud.agent.api.UnmanageInstanceCommand;
import com.cloud.agent.api.UnregisterVMCommand;
import com.cloud.agent.api.UpdateVmNicAnswer;
import com.cloud.agent.api.UpdateVmNicCommand;
import com.cloud.agent.api.VmDiskStatsEntry;
import com.cloud.agent.api.VmNetworkStatsEntry;
import com.cloud.agent.api.VmStatsEntry;
@ -6270,6 +6272,80 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac
_jobMgr.marshallResultObject(result));
}
@Override
public boolean updateVmNic(VirtualMachine vm, Nic nic, Boolean enabled) {
Outcome<VirtualMachine> outcome = updateVmNicThroughJobQueue(vm, nic, enabled);
retrieveVmFromJobOutcome(outcome, vm.getUuid(), "updateVmNic");
try {
Object jobResult = retrieveResultFromJobOutcomeAndThrowExceptionIfNeeded(outcome);
if (jobResult instanceof Boolean) {
return BooleanUtils.isTrue((Boolean) jobResult);
}
} catch (ResourceUnavailableException | InsufficientCapacityException ex) {
throw new CloudRuntimeException(String.format("Exception while updating VM [%s] NIC. Check the logs for more information.", vm.getUuid()));
}
throw new CloudRuntimeException("Unexpected job execution result.");
}
private boolean orchestrateUpdateVmNic(final VirtualMachine vm, final Nic nic, final Boolean enabled) throws ResourceUnavailableException {
if (vm.getState() == State.Running) {
try {
UpdateVmNicCommand updateVmNicCmd = new UpdateVmNicCommand(nic.getMacAddress(), vm.getName(), enabled);
Commands cmds = new Commands(Command.OnError.Stop);
cmds.addCommand("updatevmnic", updateVmNicCmd);
_agentMgr.send(vm.getHostId(), cmds);
UpdateVmNicAnswer updateVmNicAnswer = cmds.getAnswer(UpdateVmNicAnswer.class);
if (updateVmNicAnswer == null || !updateVmNicAnswer.getResult()) {
logger.warn("Unable to update VM %s NIC [{}].", vm.getName(), nic.getUuid());
return false;
}
} catch (final OperationTimedoutException e) {
throw new AgentUnavailableException(String.format("Unable to update NIC %s for VM %s.", nic.getUuid(), vm.getUuid()), vm.getHostId(), e);
}
}
NicVO nicVo = _nicsDao.findById(nic.getId());
nicVo.setEnabled(enabled);
_nicsDao.persist(nicVo);
return true;
}
public Outcome<VirtualMachine> updateVmNicThroughJobQueue(final VirtualMachine vm, final Nic nic, final Boolean isNicEnabled) {
Long vmId = vm.getId();
String commandName = VmWorkUpdateNic.class.getName();
Pair<VmWorkJobVO, Long> pendingWorkJob = retrievePendingWorkJob(vmId, commandName);
VmWorkJobVO workJob = pendingWorkJob.first();
if (workJob == null) {
Pair<VmWorkJobVO, VmWork> newVmWorkJobAndInfo = createWorkJobAndWorkInfo(commandName, vmId);
workJob = newVmWorkJobAndInfo.first();
VmWorkUpdateNic workInfo = new VmWorkUpdateNic(newVmWorkJobAndInfo.second(), nic.getId(), isNicEnabled);
setCmdInfoAndSubmitAsyncJob(workJob, workInfo, vmId);
}
AsyncJobExecutionContext.getCurrentExecutionContext().joinJob(workJob.getId());
return new VmJobVirtualMachineOutcome(workJob, vmId);
}
@ReflectionUse
private Pair<JobInfo.Status, String> orchestrateUpdateVmNic(final VmWorkUpdateNic work) throws Exception {
VMInstanceVO vm = findVmById(work.getVmId());
final NicVO nic = _entityMgr.findById(NicVO.class, work.getNicId());
if (nic == null) {
throw new CloudRuntimeException(String.format("Unable to find NIC with ID %s.", work.getNicId()));
}
final boolean result = orchestrateUpdateVmNic(vm, nic, work.isEnabled());
return new Pair<>(JobInfo.Status.SUCCEEDED, _jobMgr.marshallResultObject(result));
}
private Pair<Long, Long> findClusterAndHostIdForVmFromVolumes(long vmId) {
Long clusterId = null;
Long hostId = null;

View File

@ -0,0 +1,38 @@
// 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;
public class VmWorkUpdateNic extends VmWork {
private static final long serialVersionUID = -8957066627929113278L;
Long nicId;
Boolean enabled;
public VmWorkUpdateNic(VmWork vmWork, Long nicId, Boolean enabled) {
super(vmWork);
this.nicId = nicId;
this.enabled = enabled;
}
public Long getNicId() {
return nicId;
}
public Boolean isEnabled() {
return enabled;
}
}

View File

@ -131,6 +131,9 @@ public class NicVO implements Nic {
@Column(name = "mtu")
Integer mtu;
@Column(name = "enabled")
boolean enabled;
@Transient
transient String nsxLogicalSwitchUuid;
@ -143,6 +146,7 @@ public class NicVO implements Nic {
this.networkId = configurationId;
this.state = State.Allocated;
this.vmType = vmType;
this.enabled = true;
}
@Override
@ -397,6 +401,14 @@ public class NicVO implements Nic {
this.nsxLogicalSwitchPortUuid = nsxLogicalSwitchPortUuid;
}
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
@Override
public int hashCode() {
return new HashCodeBuilder(17, 31).append(id).toHashCode();

View File

@ -114,3 +114,6 @@ CALL `cloud`.`IDEMPOTENT_UPDATE_API_PERMISSION`('Resource Admin', 'deleteUserKey
-- Add conserve mode for VPC offerings
CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.vpc_offerings','conserve_mode', 'tinyint(1) unsigned NULL DEFAULT 0 COMMENT ''True if the VPC offering is IP conserve mode enabled, allowing public IP services to be used across multiple VPC tiers'' ');
--- Disable/enable NICs
CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.nics','enabled', 'TINYINT(1) NOT NULL DEFAULT 1 COMMENT ''Indicates whether the NIC is enabled or not'' ');

View File

@ -76,6 +76,7 @@ select
nics.broadcast_uri broadcast_uri,
nics.isolation_uri isolation_uri,
nics.mtu mtu,
nics.enabled is_nic_enabled,
vpc.id vpc_id,
vpc.uuid vpc_uuid,
vpc.name vpc_name,

View File

@ -141,6 +141,7 @@ SELECT
`nics`.`mac_address` AS `mac_address`,
`nics`.`broadcast_uri` AS `broadcast_uri`,
`nics`.`isolation_uri` AS `isolation_uri`,
`nics`.`enabled` AS `is_nic_enabled`,
`vpc`.`id` AS `vpc_id`,
`vpc`.`uuid` AS `vpc_uuid`,
`networks`.`uuid` AS `network_uuid`,

View File

@ -275,6 +275,7 @@ public class BridgeVifDriver extends VifDriverBase {
if (nic.getPxeDisable()) {
intf.setPxeDisable(true);
}
intf.setLinkStateUp(nic.isEnabled());
return intf;
}

View File

@ -4851,6 +4851,12 @@ public class LibvirtComputingResource extends ServerResourceBase implements Serv
}
}
public InterfaceDef getInterface(final Connect conn, final String vmName, final String macAddress) {
List<InterfaceDef> interfaces = getInterfaces(conn, vmName);
return interfaces.stream().filter(interfaceDef -> interfaceDef.getMacAddress().equals(macAddress))
.findFirst().orElseThrow(() -> new CloudRuntimeException(String.format("Unable to find NIC with MAC address %s.", macAddress)));
}
public List<DiskDef> getDisks(final Connect conn, final String vmName) {
final LibvirtDomainXMLParser parser = new LibvirtDomainXMLParser();
Domain dm = null;

View File

@ -0,0 +1,67 @@
//
// 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.UpdateVmNicAnswer;
import com.cloud.agent.api.UpdateVmNicCommand;
import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource;
import com.cloud.hypervisor.kvm.resource.LibvirtVMDef.InterfaceDef;
import com.cloud.resource.CommandWrapper;
import com.cloud.resource.ResourceWrapper;
import org.libvirt.Connect;
import org.libvirt.Domain;
import org.libvirt.LibvirtException;
@ResourceWrapper(handles = UpdateVmNicCommand.class)
public final class LibvirtUpdateVmNicCommandWrapper extends CommandWrapper<UpdateVmNicCommand, Answer, LibvirtComputingResource> {
@Override
public Answer execute(UpdateVmNicCommand command, LibvirtComputingResource libvirtComputingResource) {
String nicMacAddress = command.getNicMacAddress();
String vmName = command.getVmName();
boolean isEnabled = command.isEnabled();
Domain vm = null;
try {
final LibvirtUtilitiesHelper libvirtUtilitiesHelper = libvirtComputingResource.getLibvirtUtilitiesHelper();
final Connect conn = libvirtUtilitiesHelper.getConnectionByVmName(vmName);
vm = libvirtComputingResource.getDomain(conn, vmName);
final InterfaceDef nic = libvirtComputingResource.getInterface(conn, vmName, nicMacAddress);
nic.setLinkStateUp(isEnabled);
vm.updateDeviceFlags(nic.toString(), Domain.DeviceModifyFlags.LIVE);
return new UpdateVmNicAnswer(command, true, "success");
} catch (final LibvirtException e) {
final String msg = String.format(" Update NIC failed due to %s.", e);
logger.warn(msg, e);
return new UpdateVmNicAnswer(command, false, msg);
} finally {
if (vm != null) {
try {
vm.free();
} catch (final LibvirtException l) {
logger.trace("Ignoring libvirt error.", l);
}
}
}
}
}

View File

@ -7203,4 +7203,35 @@ public class LibvirtComputingResourceTest {
libvirtComputingResourceSpy.defineDiskForDefaultPoolType(diskDef, volume, false, false, false, physicalDisk, DEV_ID, DISK_BUS_TYPE, DISK_BUS_TYPE_DATA, null);
Mockito.verify(diskDef).defFileBasedDisk(PHYSICAL_DISK_PATH, DEV_ID, DISK_BUS_TYPE_DATA, DiskDef.DiskFmtType.QCOW2);
}
@Test
public void getInterfaceTestValidMacAddressReturnInterface() {
String macAddress = "a0:90:27:a9:9e:62";
final String vmName = "Test";
final InterfaceDef interfaceDef = Mockito.mock(InterfaceDef.class);
final List<InterfaceDef> interfaces = new ArrayList<>();
interfaces.add(interfaceDef);
Mockito.doReturn(macAddress).when(interfaceDef).getMacAddress();
Mockito.doReturn(interfaces).when(libvirtComputingResourceSpy).getInterfaces(Mockito.any(), Mockito.anyString());
InterfaceDef result = libvirtComputingResourceSpy.getInterface(connMock, vmName, macAddress);
Assert.assertNotNull(result);
}
@Test(expected = CloudRuntimeException.class)
public void getInterfaceTestInvalidMacAddressThrowCloudRuntimeException() {
String invalidMacAddress = "ea:57:5d:f1:64:05";
String macAddress = "a0:90:27:a9:9e:62";
final String vmName = "Test";
final InterfaceDef interfaceDef = Mockito.mock(InterfaceDef.class);
final List<InterfaceDef> interfaces = new ArrayList<>();
interfaces.add(interfaceDef);
Mockito.doReturn(macAddress).when(interfaceDef).getMacAddress();
Mockito.doReturn(interfaces).when(libvirtComputingResourceSpy).getInterfaces(Mockito.any(), Mockito.anyString());
libvirtComputingResourceSpy.getInterface(connMock, vmName, invalidMacAddress);
}
}

View File

@ -2230,6 +2230,10 @@ public class ApiDBUtils {
return s_nicSecondaryIpDao.listByNicId(nicId);
}
public static NicVO findNicById(long nicId) {
return s_nicDao.findById(nicId);
}
public static TemplateResponse newTemplateUpdateResponse(TemplateJoinVO vr) {
return s_templateJoinDao.newUpdateResponse(vr);
}

View File

@ -1929,6 +1929,12 @@ public class ApiResponseHelper implements ResponseGenerator {
}
@Override
public UserVm findUserVmByNicId(Long nicId) {
NicVO nic = ApiDBUtils.findNicById(nicId);
return ApiDBUtils.findUserVmById(nic.getInstanceId());
}
@Override
public VolumeVO findVolumeById(Long volumeId) {
return ApiDBUtils.findVolumeById(volumeId);
@ -4844,6 +4850,8 @@ public class ApiResponseHelper implements ResponseGenerator {
VpcVO vpc = _entityMgr.findByUuidIncludingRemoved(VpcVO.class, userVm.getVpcUuid());
response.setVpcName(vpc.getName());
}
response.setEnabled(result.isEnabled());
return response;
}

View File

@ -196,6 +196,7 @@ public class DomainRouterJoinDaoImpl extends GenericDaoBase<DomainRouterJoinVO,
nicResponse.setMtu(router.getMtu());
}
nicResponse.setIsDefault(router.isDefaultNic());
nicResponse.setEnabled(router.isNicEnabled());
nicResponse.setObjectName("nic");
routerResponse.addNic(nicResponse);
}
@ -289,6 +290,7 @@ public class DomainRouterJoinDaoImpl extends GenericDaoBase<DomainRouterJoinVO,
nicResponse.setMtu(vr.getMtu());
}
nicResponse.setIsDefault(vr.isDefaultNic());
nicResponse.setEnabled(vr.isNicEnabled());
nicResponse.setObjectName("nic");
vrData.addNic(nicResponse);
}

View File

@ -358,6 +358,7 @@ public class UserVmJoinDaoImpl extends GenericDaoBaseWithTagInformation<UserVmJo
nicResponse.setIp6Address(userVm.getIp6Address());
nicResponse.setIp6Gateway(userVm.getIp6Gateway());
nicResponse.setIp6Cidr(userVm.getIp6Cidr());
nicResponse.setEnabled(userVm.isNicEnabled());
if (userVm.getBroadcastUri() != null) {
nicResponse.setBroadcastUri(userVm.getBroadcastUri().toString());
}
@ -625,6 +626,7 @@ public class UserVmJoinDaoImpl extends GenericDaoBaseWithTagInformation<UserVmJo
/*17: default*/
nicResponse.setIsDefault(uvo.isDefaultNic());
nicResponse.setDeviceId(String.valueOf(uvo.getNicDeviceId()));
nicResponse.setEnabled(uvo.isNicEnabled());
List<NicSecondaryIpVO> secondaryIps = ApiDBUtils.findNicSecondaryIps(uvo.getNicId());
if (secondaryIps != null) {
List<NicSecondaryIpResponse> ipList = new ArrayList<NicSecondaryIpResponse>();

View File

@ -274,6 +274,9 @@ public class DomainRouterJoinVO extends BaseViewVO implements ControlledViewEnti
@Column(name = "mtu")
private Integer mtu;
@Column(name = "is_nic_enabled")
private boolean isNicEnabled;
public DomainRouterJoinVO() {
}
@ -577,4 +580,8 @@ public class DomainRouterJoinVO extends BaseViewVO implements ControlledViewEnti
public Integer getMtu() {
return mtu;
}
public boolean isNicEnabled() {
return isNicEnabled;
}
}

View File

@ -345,6 +345,9 @@ public class UserVmJoinVO extends BaseViewWithTagInformationVO implements Contro
@Column(name = "is_default_nic")
private boolean isDefaultNic;
@Column(name = "is_nic_enabled")
private boolean isNicEnabled;
@Column(name = "ip_address")
private String ipAddress;
@ -1089,4 +1092,8 @@ public class UserVmJoinVO extends BaseViewWithTagInformationVO implements Contro
public String getLeaseActionExecution() {
return leaseActionExecution;
}
public boolean isNicEnabled() {
return isNicEnabled;
}
}

View File

@ -205,6 +205,7 @@ public abstract class HypervisorGuruBase extends AdapterBase implements Hypervis
to.setIp6Dns1(profile.getIPv6Dns1());
to.setIp6Dns2(profile.getIPv6Dns2());
to.setNetworkId(profile.getNetworkId());
to.setEnabled(profile.isEnabled());
NetworkVO network = networkDao.findById(profile.getNetworkId());
to.setNetworkUuid(network.getUuid());

View File

@ -567,6 +567,7 @@ import org.apache.cloudstack.api.command.user.vm.StartVMCmd;
import org.apache.cloudstack.api.command.user.vm.StopVMCmd;
import org.apache.cloudstack.api.command.user.vm.UpdateDefaultNicForVMCmd;
import org.apache.cloudstack.api.command.user.vm.UpdateVMCmd;
import org.apache.cloudstack.api.command.user.vm.UpdateVmNicCmd;
import org.apache.cloudstack.api.command.user.vm.UpdateVmNicIpCmd;
import org.apache.cloudstack.api.command.user.vm.UpgradeVMCmd;
import org.apache.cloudstack.api.command.user.vmgroup.CreateVMGroupCmd;
@ -4165,6 +4166,7 @@ public class ManagementServerImpl extends MutualExclusiveIdsManagerBase implemen
cmdList.add(StopVMCmd.class);
cmdList.add(UpdateDefaultNicForVMCmd.class);
cmdList.add(UpdateVMCmd.class);
cmdList.add(UpdateVmNicCmd.class);
cmdList.add(UpgradeVMCmd.class);
cmdList.add(CreateVMGroupCmd.class);
cmdList.add(DeleteVMGroupCmd.class);

View File

@ -99,6 +99,7 @@ import org.apache.cloudstack.api.command.user.vm.SecurityGroupAction;
import org.apache.cloudstack.api.command.user.vm.StartVMCmd;
import org.apache.cloudstack.api.command.user.vm.UpdateDefaultNicForVMCmd;
import org.apache.cloudstack.api.command.user.vm.UpdateVMCmd;
import org.apache.cloudstack.api.command.user.vm.UpdateVmNicCmd;
import org.apache.cloudstack.api.command.user.vm.UpdateVmNicIpCmd;
import org.apache.cloudstack.api.command.user.vm.UpgradeVMCmd;
import org.apache.cloudstack.api.command.user.vmgroup.CreateVMGroupCmd;
@ -1899,6 +1900,54 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
return vm;
}
@Override
public UserVm updateVirtualMachineNic(UpdateVmNicCmd cmd) {
Long nicId = cmd.getNicId();
Boolean isNicEnabled = cmd.isEnabled();
Account caller = CallContext.current().getCallingAccount();
NicVO nic = _nicDao.findById(nicId);
if (nic == null) {
throw new InvalidParameterValueException("Unable to find the specified NIC.");
}
UserVmVO vmInstance = _vmDao.findById(nic.getInstanceId());
if (vmInstance == null) {
throw new InvalidParameterValueException("Unable to find a virtual machine associated with the specified NIC.");
}
if (vmInstance.getHypervisorType() != HypervisorType.KVM) {
throw new InvalidParameterValueException("Updating the VM NIC is only supported by the KVM hypervisor.");
}
NetworkVO network = _networkDao.findById(nic.getNetworkId());
if (network == null) {
throw new InvalidParameterValueException("Unable to find NIC's network.");
}
_accountMgr.checkAccess(caller, null, true, vmInstance);
if (isNicEnabled == null) {
return vmInstance;
}
boolean success = false;
try {
success = _itMgr.updateVmNic(vmInstance, nic, isNicEnabled);
} catch (ResourceUnavailableException e) {
throw new CloudRuntimeException(String.format("Unable to update NIC %s of VM %s in network %s due to: %s.", nic, vmInstance, network.getUuid(), e.getMessage()));
} catch (ConcurrentOperationException e) {
throw new CloudRuntimeException(String.format("Concurrent operations while updating NIC %s for VM %s: %s.", nic, vmInstance, e.getMessage()));
}
if (!success) {
throw new CloudRuntimeException(String.format("Failed to update NIC %s of VM %s in network %s.", nic, vmInstance, network.getUuid()));
}
logger.debug("Successfully updated NIC {} in network {} for VM {}.", nic, network.getUuid(), vmInstance);
return vmInstance;
}
private void updatePublicIpDnatVmIp(long vmId, long networkId, String oldIp, String newIp) {
if (!_networkModel.areServicesSupportedInNetwork(networkId, Service.StaticNat)) {
return;

View File

@ -77,6 +77,7 @@ import org.apache.cloudstack.api.command.user.vm.ResetVMSSHKeyCmd;
import org.apache.cloudstack.api.command.user.vm.ResetVMUserDataCmd;
import org.apache.cloudstack.api.command.user.vm.RestoreVMCmd;
import org.apache.cloudstack.api.command.user.vm.UpdateVMCmd;
import org.apache.cloudstack.api.command.user.vm.UpdateVmNicCmd;
import org.apache.cloudstack.api.command.user.volume.ResizeVolumeCmd;
import org.apache.cloudstack.backup.BackupManager;
import org.apache.cloudstack.backup.BackupVO;
@ -245,6 +246,9 @@ public class UserVmManagerImplTest {
@Mock
private UpdateVMCmd updateVmCommand;
@Mock
private UpdateVmNicCmd updateVmNicCmd;
@Mock
private AccountManager accountManager;
@ -272,6 +276,9 @@ public class UserVmManagerImplTest {
@Mock
private UserVO callerUser;
@Mock
private NicVO nicMock;
@Mock
private VMTemplateDao templateDao;
@ -455,6 +462,8 @@ public class UserVmManagerImplTest {
private static final long vmId = 1l;
private static final long zoneId = 2L;
private static final long accountId = 3L;
private static final long nicId = 4L;
private static final long networkId = 5L;
private static final long serviceOfferingId = 10L;
private static final long templateId = 11L;
private static final long volumeId = 1L;
@ -4254,6 +4263,61 @@ public class UserVmManagerImplTest {
verify(userVmManagerImpl, never()).addExtraConfig(any(UserVmVO.class), anyString());
}
@Test(expected = InvalidParameterValueException.class)
public void updateVirtualMachineNicTestInvalidNicThrowInvalidParameterValueException() {
Long invalidId = -1L;
Mockito.doReturn(invalidId).when(updateVmNicCmd).getNicId();
userVmManagerImpl.updateVirtualMachineNic(updateVmNicCmd);
}
@Test(expected = InvalidParameterValueException.class)
public void updateVirtualMachineNicTestInvalidNicUserVmThrowInvalidParameterValueException() {
Mockito.doReturn(nicId).when(updateVmNicCmd).getNicId();
Mockito.doReturn(nicMock).when(nicDao).findById(nicId);
userVmManagerImpl.updateVirtualMachineNic(updateVmNicCmd);
}
@Test(expected = InvalidParameterValueException.class)
public void updateVirtualMachineNicTestInvalidNicNetworkThrowInvalidParameterValueException() {
Mockito.doReturn(nicId).when(updateVmNicCmd).getNicId();
Mockito.doReturn(true).when(updateVmNicCmd).isEnabled();
Mockito.doReturn(nicMock).when(nicDao).findById(nicId);
Mockito.doReturn(vmId).when(nicMock).getInstanceId();
Mockito.doReturn(userVmVoMock).when(userVmDao).findById(vmId);
userVmManagerImpl.updateVirtualMachineNic(updateVmNicCmd);
}
@Test(expected = CloudRuntimeException.class)
public void updateVirtualMachineNicTestInvalidNicNetworkThrowCloudRuntimeException() {
Mockito.doReturn(nicId).when(updateVmNicCmd).getNicId();
Mockito.doReturn(true).when(updateVmNicCmd).isEnabled();
Mockito.doReturn(nicMock).when(nicDao).findById(nicId);
Mockito.doReturn(vmId).when(nicMock).getInstanceId();
Mockito.doReturn(userVmVoMock).when(userVmDao).findById(vmId);
userVmManagerImpl.updateVirtualMachineNic(updateVmNicCmd);
}
@Test
public void updateVirtualMachineNicTestValidInputReturnNicUserVm() throws ResourceUnavailableException {
Mockito.doReturn(nicId).when(updateVmNicCmd).getNicId();
Mockito.doReturn(true).when(updateVmNicCmd).isEnabled();
Mockito.doReturn(nicMock).when(nicDao).findById(nicId);
Mockito.doReturn(vmId).when(nicMock).getInstanceId();
Mockito.doReturn(userVmVoMock).when(userVmDao).findById(vmId);
Mockito.doReturn(Hypervisor.HypervisorType.KVM).when(userVmVoMock).getHypervisorType();
Mockito.doReturn(networkId).when(nicMock).getNetworkId();
Mockito.doReturn(networkMock).when(_networkDao).findById(networkId);
Mockito.doReturn(true).when(virtualMachineManager).updateVmNic(Mockito.any(), Mockito.any(), Mockito.any());
UserVm result = userVmManagerImpl.updateVirtualMachineNic(updateVmNicCmd);
Assert.assertNotNull(result);
}
@Test
public void testTransitionExpungingToErrorVmInExpungingState() throws Exception {
UserVmVO vm = mock(UserVmVO.class);

View File

@ -174,6 +174,7 @@ known_categories = {
'removeIpFromNic': 'Nic',
'updateVmNicIp': 'Nic',
'listNics':'Nic',
'updateVmNic': 'Nic',
'AffinityGroup': 'Affinity Group',
'ImageStore': 'Image Store',
'addImageStore': 'Image Store',

View File

@ -993,6 +993,7 @@
"label.edit.acl": "Edit ACL",
"label.edit.acl.rule": "Edit ACL rule",
"label.edit.autoscale.vmprofile": "Edit AutoScale Instance Profile",
"label.edit.nic": "Edit NIC",
"label.edit.project.details": "Edit project details",
"label.edit.project.role": "Edit project role",
"label.edit.role": "Edit Role",
@ -3735,6 +3736,7 @@
"message.network.selection": "Choose one or more Networks to attach the Instance to.",
"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.",
"message.network.usage.info.data.points": "Each data point represents the difference in data traffic since the last data point.",
"message.network.usage.info.sum.of.vnics": "The Network usage shown is made up of the sum of data traffic from all the vNICs in the Instance.",
"message.nfs.mount.options.description": "Comma separated list of NFS mount options for KVM hosts. Supported options : vers=[3,4.0,4.1,4.2], nconnect=[1...16]",
@ -4018,13 +4020,14 @@
"message.success.delete.vgpu.profile": "Successfully deleted vGPU profile",
"message.success.update.custom.action": "Successfully updated Custom Action",
"message.success.update.extension": "Successfully updated Extension",
"message.success.update.sharedfs": "Successfully updated Shared FileSystem",
"message.success.update.ipaddress": "Successfully updated IP address",
"message.success.update.iprange": "Successfully updated IP range",
"message.success.update.ipv4.subnet": "Successfully updated IPv4 subnet",
"message.success.update.iso": "Successfully updated ISO",
"message.success.update.kubeversion": "Successfully updated Kubernetes supported version",
"message.success.update.network": "Successfully updated Network",
"message.success.update.nic": "Successfully updated NIC",
"message.success.update.sharedfs": "Successfully updated Shared FileSystem",
"message.success.update.template": "Successfully updated Template",
"message.success.update.user": "Successfully updated User",
"message.success.update.vpn.customer.gateway": "Successfully updated VPN Customer Gateway",
@ -4074,6 +4077,7 @@
"message.two.fa.setup.page": "Two factor authentication (2FA) is an extra layer of security to your account.<br> Once setup is done, on every login you will be prompted to enter the 2FA code.<br>",
"message.two.fa.view.setup.key": "Click here to view the setup key",
"message.two.fa.view.static.pin": "Click here to view the static PIN",
"message.update.nic.processing": "Updating NIC...",
"message.update.ipaddress.processing": "Updating IP Address...",
"message.update.resource.count": "Please confirm that you want to update resource counts for this Account.",
"message.update.resource.count.domain": "Please confirm that you want to update resource counts for this domain.",

View File

@ -623,6 +623,7 @@
"label.edit": "Editar",
"label.edit.acl": "Editar lista ACL",
"label.edit.acl.rule": "Editar regra ACL",
"label.edit.nic": "Editar NIC",
"label.edit.project.details": "Editar detalhes do projeto",
"label.edit.project.role": "Editar fun\u00e7\u00e3o do projeto",
"label.edit.role": "Editar fun\u00e7\u00e3o",
@ -2298,6 +2299,7 @@
"message.network.removenic": "Por favor, confirme que deseja remover esta interface de rede, esta a\u00e7\u00e3o tamb\u00e9m ir\u00e1 remover a rede associada \u00e0 VM.",
"message.network.secondaryip": "Por favor, confirme que voc\u00ea gostaria de adquirir um novo IP secund\u00e1rio para este NIC. \n NOTA: Voc\u00ea precisa configurar manualmente o IP secund\u00e1rio rec\u00e9m-adquirido dentro da m\u00e1quina virtual.",
"message.network.updateip": "Por favor, confirme que voc\u00ea gostaria de mudar o endere\u00e7o IP da NIC em VM.",
"message.network.update.nic": "Por favor, confirme que voc\u00ea gostaria de atualizar esta NIC.",
"message.network.usage.info.data.points": "Cada ponto no gr\u00e1fico representa a diferen\u00e7a de dados trafegados desde a \u00faltima coleta de estat\u00edstica realizada (o ponto anterior)",
"message.network.usage.info.sum.of.vnics": "O uso de rede apresentado \u00e9 composto pela soma de dados trafegados por todas as vNICs da VM",
"message.no.data.to.show.for.period": "Nenhum dado para mostrar no per\u00edodo selecionado.",
@ -2455,6 +2457,7 @@
"message.success.update.iprange": "Intervalo de IP atualizado com sucesso",
"message.success.update.kubeversion": "Vers\u00e3o compat\u00edvel com Kubernetes atualizada com sucesso",
"message.success.update.network": "Rede atualizada com sucesso",
"message.success.update.nic": "NIC atualizada com sucesso",
"message.success.update.user": "Usu\u00e1rio atualizado com sucesso",
"message.success.upgrade.kubernetes": "Cluster do Kubernetes atualizado com sucesso",
"message.success.upload": "Carregado com sucesso",
@ -2471,6 +2474,7 @@
"message.template.iso": "Selecione o template ou ISO para continuar",
"message.tooltip.reserved.system.netmask": "O prefixo de rede que define a subrede deste pod. utilize a nota\u00e7\u00e3o CIDR.",
"message.traffic.type.to.basic.zone": "Tipo de tr\u00e1fego para a zona b\u00e1sica",
"message.update.nic.processing": "Atualizando NIC...",
"message.update.ipaddress.processing": "Atualizando endere\u00e7o IP...",
"message.update.resource.count": "Por favor confirme que voc\u00ea quer atualizar a contagem de recursos para esta conta.",
"message.update.resource.count.domain": "Por favor, confirme que voc\u00ea deseja atualizar as contagens de recursos para este dom\u00ednio.",

View File

@ -54,6 +54,13 @@
icon="environment-outlined"
:disabled="(!('addIpToNic' in $store.getters.apis) && !('addIpToNic' in $store.getters.apis))"
@onClick="onAcquireSecondaryIPAddress(record)" />
<tooltip-button
v-if="resource.hypervisor === 'KVM'"
tooltipPlacement="bottom"
:tooltip="$t('label.edit.nic')"
icon="edit-outlined"
:disabled="(!('updateVmNic' in $store.getters.apis))"
@onClick="onUpdateNic(record)" />
<a-popconfirm
:title="$t('message.network.removenic')"
@confirm="removeNIC(record.nic)"
@ -195,7 +202,7 @@
<a-divider />
<div v-ctrl-enter="submitSecondaryIP">
<div class="modal-form">
<p class="modal-form__label">{{ $t('label.publicip') }}:</p>
<p class="modal-form__label--no-margin">{{ $t('label.publicip') }}:</p>
<a-select
v-if="editNicResource.type==='Shared'"
v-model:value="newSecondaryIp"
@ -243,6 +250,31 @@
</a-list-item>
</a-list>
</a-modal>
<a-modal
:visible="showUpdateNicModal"
:title="$t('label.edit.nic')"
:maskClosable="false"
:closable="true"
:footer="null"
@cancel="closeModals"
>
{{ $t('message.network.update.nic') }}
<a-form
@finish="submitUpdateNic"
v-ctrl-enter="submitUpdateNic">
<a-form-item name="linkstate" ref="linkstate">
<p class="modal-form__label">{{ $t('state.enabled') }}:</p>
<a-switch v-model:checked="editNicStateValue" @change="val => { editNicStateValue = val }" />
</a-form-item>
<div :span="24" class="action-button">
<a-button @click="closeModals">{{ $t('label.cancel') }}</a-button>
<a-button type="primary" ref="submit" @click="submitUpdateNic">{{ $t('label.ok') }}</a-button>
</div>
</a-form>
</a-modal>
</a-spin>
</template>
@ -279,6 +311,7 @@ export default {
showAddNetworkModal: false,
showUpdateIpModal: false,
showSecondaryIpModal: false,
showUpdateNicModal: false,
addNetworkData: {
allNetworks: [],
network: '',
@ -289,6 +322,7 @@ export default {
editIpAddressNic: '',
editIpAddressValue: '',
editNetworkId: '',
editNicStateValue: false,
secondaryIPs: [],
selectedNicId: '',
newSecondaryIp: '',
@ -360,6 +394,7 @@ export default {
this.showAddNetworkModal = false
this.showUpdateIpModal = false
this.showSecondaryIpModal = false
this.showUpdateNicModal = false
this.addNetworkData.network = ''
this.addNetworkData.ipaddress = ''
this.addNetworkData.macaddress = ''
@ -386,6 +421,11 @@ export default {
this.editNetworkId = record.nic.networkid
this.fetchSecondaryIPs(record.nic.id)
},
onUpdateNic (record) {
this.editNicResource = record.nic
this.editNicStateValue = record.nic.enabled
this.showUpdateNicModal = true
},
submitAddNetwork () {
if (this.loadingNic) return
const params = {}
@ -609,12 +649,46 @@ export default {
this.loadingNic = false
this.fetchSecondaryIPs(this.selectedNicId)
})
},
submitUpdateNic () {
if (this.loadingNic) return
this.loadingNic = true
this.showUpdateNicModal = false
const params = {
nicId: this.editNicResource.id,
enabled: this.editNicStateValue
}
postAPI('updateVmNic', params).then(response => {
this.$pollJob({
jobId: response.updatevmnicresponse.jobid,
successMessage: this.$t('message.success.update.nic'),
successMethod: () => {
this.loadingNic = false
this.closeModals()
},
errorMessage: this.$t('label.error'),
errorMethod: () => {
this.loadingNic = false
this.closeModals()
},
loadingMessage: this.$t('message.update.nic.processing'),
catchMessage: this.$t('error.fetching.async.job.result'),
catchMethod: () => {
this.loadingNic = false
this.closeModals()
this.$emit('refresh')
}
})
}).catch(error => {
this.$notifyError(error)
this.loadingNic = false
})
}
}
}
</script>
<style scoped>
<style lang="scss" scoped>
.modal-form {
display: flex;
flex-direction: column;
@ -626,6 +700,7 @@ export default {
&--no-margin {
margin-top: 0;
font-weight: bold;
}
}
}

View File

@ -71,6 +71,9 @@
{{ $t('label.default') }}
</a-tag>
</template>
<template v-if="column.key === 'enabled'">
<status :text="text ? 'enabled' : 'disabled'"/> {{ text ? 'Enabled' : 'Disabled' }}
</template>
</template>
</a-table>
</template>
@ -78,6 +81,7 @@
<script>
import { getAPI } from '@/api'
import ResourceIcon from '@/components/view/ResourceIcon'
import Status from '@/components/widgets/Status'
export default {
name: 'NicsTable',
@ -92,7 +96,8 @@ export default {
}
},
components: {
ResourceIcon
ResourceIcon,
Status
},
inject: ['parentFetchData'],
data () {
@ -123,6 +128,11 @@ export default {
{
title: this.$t('label.gateway'),
dataIndex: 'gateway'
},
{
key: 'enabled',
title: this.$t('label.state'),
dataIndex: 'enabled'
}
],
networkicon: {},