mirror of
https://github.com/apache/cloudstack.git
synced 2025-10-26 08:42:29 +01:00
Add live migration of system VMs (KVM) (#6491)
Co-authored-by: Rodrigo D. Lopez <19981369+RodrigoDLopez@users.noreply.github.com>
This commit is contained in:
parent
f580a8d7a2
commit
23033fbb74
@ -434,6 +434,8 @@ public interface UserVmService {
|
||||
|
||||
UserVm getUserVm(long vmId);
|
||||
|
||||
VirtualMachine getVm(long vmId);
|
||||
|
||||
/**
|
||||
* Migrate the given VM to the destination host provided. The API returns the migrated VM if migration succeeds.
|
||||
* Only Root
|
||||
|
||||
@ -455,6 +455,7 @@ public class ApiConstants {
|
||||
public static final String VM_AVAILABLE = "vmavailable";
|
||||
public static final String VM_LIMIT = "vmlimit";
|
||||
public static final String VM_TOTAL = "vmtotal";
|
||||
public static final String VM_TYPE = "vmtype";
|
||||
public static final String VNET = "vnet";
|
||||
public static final String IS_VOLATILE = "isvolatile";
|
||||
public static final String VOLUME_ID = "volumeid";
|
||||
@ -619,6 +620,7 @@ public class ApiConstants {
|
||||
public static final String TRAFFIC_TYPE_IMPLEMENTOR = "traffictypeimplementor";
|
||||
public static final String KEYWORD = "keyword";
|
||||
public static final String LIST_ALL = "listall";
|
||||
public static final String LIST_SYSTEM_VMS = "listsystemvms";
|
||||
public static final String IP_RANGES = "ipranges";
|
||||
public static final String IPV6_ROUTING = "ip6routing";
|
||||
public static final String IPV6_ROUTES = "ip6routes";
|
||||
|
||||
@ -30,6 +30,7 @@ import org.apache.cloudstack.api.Parameter;
|
||||
import org.apache.cloudstack.api.ResponseObject.ResponseView;
|
||||
import org.apache.cloudstack.api.ServerApiException;
|
||||
import org.apache.cloudstack.api.response.HostResponse;
|
||||
import org.apache.cloudstack.api.response.SystemVmResponse;
|
||||
import org.apache.cloudstack.api.response.UserVmResponse;
|
||||
import org.apache.commons.collections.MapUtils;
|
||||
import org.apache.log4j.Logger;
|
||||
@ -152,20 +153,17 @@ public class MigrateVirtualMachineWithVolumeCmd extends BaseAsyncCmd {
|
||||
@Override
|
||||
public void execute() {
|
||||
if (hostId == null && MapUtils.isEmpty(migrateVolumeTo)) {
|
||||
throw new InvalidParameterValueException(String.format("Either %s or %s must be passed for migrating the VM", ApiConstants.HOST_ID, ApiConstants.MIGRATE_TO));
|
||||
throw new InvalidParameterValueException(String.format("Either %s or %s must be passed for migrating the VM.", ApiConstants.HOST_ID, ApiConstants.MIGRATE_TO));
|
||||
}
|
||||
|
||||
UserVm userVm = _userVmService.getUserVm(getVirtualMachineId());
|
||||
if (userVm == null) {
|
||||
throw new InvalidParameterValueException("Unable to find the VM by id=" + getVirtualMachineId());
|
||||
VirtualMachine virtualMachine = _userVmService.getVm(getVirtualMachineId());
|
||||
if (!VirtualMachine.State.Running.equals(virtualMachine.getState()) && hostId != null) {
|
||||
throw new InvalidParameterValueException(String.format("%s is not in the Running state to migrate it to the new host.", virtualMachine));
|
||||
}
|
||||
|
||||
if (!VirtualMachine.State.Running.equals(userVm.getState()) && hostId != null) {
|
||||
throw new InvalidParameterValueException(String.format("VM ID: %s is not in Running state to migrate it to new host", userVm.getUuid()));
|
||||
}
|
||||
|
||||
if (!VirtualMachine.State.Stopped.equals(userVm.getState()) && hostId == null) {
|
||||
throw new InvalidParameterValueException(String.format("VM ID: %s is not in Stopped state to migrate, use %s parameter to migrate it to a new host", userVm.getUuid(), ApiConstants.HOST_ID));
|
||||
if (!VirtualMachine.State.Stopped.equals(virtualMachine.getState()) && hostId == null) {
|
||||
throw new InvalidParameterValueException(String.format("%s is not in the Stopped state to migrate, use the %s parameter to migrate it to a new host.",
|
||||
virtualMachine, ApiConstants.HOST_ID));
|
||||
}
|
||||
|
||||
try {
|
||||
@ -174,16 +172,15 @@ public class MigrateVirtualMachineWithVolumeCmd extends BaseAsyncCmd {
|
||||
Host destinationHost = _resourceService.getHost(getHostId());
|
||||
// OfflineVmwareMigration: destination host would have to not be a required parameter for stopped VMs
|
||||
if (destinationHost == null) {
|
||||
throw new InvalidParameterValueException("Unable to find the host to migrate the VM, host id =" + getHostId());
|
||||
s_logger.error(String.format("Unable to find the host with ID [%s].", getHostId()));
|
||||
throw new InvalidParameterValueException("Unable to find the specified host to migrate the VM.");
|
||||
}
|
||||
migratedVm = _userVmService.migrateVirtualMachineWithVolume(getVirtualMachineId(), destinationHost, getVolumeToPool());
|
||||
} else if (MapUtils.isNotEmpty(migrateVolumeTo)) {
|
||||
migratedVm = _userVmService.vmStorageMigration(getVirtualMachineId(), getVolumeToPool());
|
||||
}
|
||||
if (migratedVm != null) {
|
||||
UserVmResponse response = _responseGenerator.createUserVmResponse(ResponseView.Full, "virtualmachine", (UserVm)migratedVm).get(0);
|
||||
response.setResponseName(getCommandName());
|
||||
setResponseObject(response);
|
||||
setResponseBasedOnVmType(virtualMachine, migratedVm);
|
||||
} else {
|
||||
throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to migrate vm");
|
||||
}
|
||||
@ -195,4 +192,16 @@ public class MigrateVirtualMachineWithVolumeCmd extends BaseAsyncCmd {
|
||||
throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void setResponseBasedOnVmType(VirtualMachine virtualMachine, VirtualMachine migratedVm) {
|
||||
if (VirtualMachine.Type.User.equals(virtualMachine.getType())) {
|
||||
UserVmResponse userVmResponse = _responseGenerator.createUserVmResponse(ResponseView.Full, "virtualmachine", (UserVm) migratedVm).get(0);
|
||||
userVmResponse.setResponseName(getCommandName());
|
||||
setResponseObject(userVmResponse);
|
||||
return;
|
||||
}
|
||||
SystemVmResponse systemVmResponse = _responseGenerator.createSystemVmResponse(migratedVm);
|
||||
systemVmResponse.setResponseName(getCommandName());
|
||||
setResponseObject(systemVmResponse);
|
||||
}
|
||||
}
|
||||
|
||||
@ -88,6 +88,10 @@ public class ListVolumesCmd extends BaseListTaggedResourcesCmd implements UserCm
|
||||
RoleType.Admin})
|
||||
private Boolean display;
|
||||
|
||||
@Parameter(name = ApiConstants.LIST_SYSTEM_VMS, type = CommandType.BOOLEAN, description = "list system VMs; only ROOT admin is eligible to pass this parameter", since = "4.18",
|
||||
authorized = { RoleType.Admin })
|
||||
private Boolean listSystemVms;
|
||||
|
||||
@Parameter(name = ApiConstants.STATE, type = CommandType.STRING, description = "state of the volume. Possible values are: Ready, Allocated, Destroy, Expunging, Expunged.")
|
||||
private String state;
|
||||
|
||||
@ -135,6 +139,10 @@ public class ListVolumesCmd extends BaseListTaggedResourcesCmd implements UserCm
|
||||
return storageId;
|
||||
}
|
||||
|
||||
public Boolean getListSystemVms() {
|
||||
return listSystemVms;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean getDisplay() {
|
||||
if (display != null) {
|
||||
|
||||
@ -96,6 +96,10 @@ public class VolumeResponse extends BaseResponseWithTagInformation implements Co
|
||||
@Param(description = "state of the virtual machine")
|
||||
private String virtualMachineState;
|
||||
|
||||
@SerializedName(ApiConstants.VM_TYPE)
|
||||
@Param(description = "type of the virtual machine")
|
||||
private String vmType;
|
||||
|
||||
@SerializedName(ApiConstants.PROVISIONINGTYPE)
|
||||
@Param(description = "provisioning type used to create volumes.")
|
||||
private String provisioningType;
|
||||
@ -333,6 +337,10 @@ public class VolumeResponse extends BaseResponseWithTagInformation implements Co
|
||||
this.volumeType = volumeType;
|
||||
}
|
||||
|
||||
public void setVmType(String vmType) {
|
||||
this.vmType = vmType;
|
||||
}
|
||||
|
||||
public void setDeviceId(Long deviceId) {
|
||||
this.deviceId = deviceId;
|
||||
}
|
||||
@ -666,6 +674,10 @@ public class VolumeResponse extends BaseResponseWithTagInformation implements Co
|
||||
return state;
|
||||
}
|
||||
|
||||
public String getVmType() {
|
||||
return vmType;
|
||||
}
|
||||
|
||||
public String getAccountName() {
|
||||
return accountName;
|
||||
}
|
||||
|
||||
@ -0,0 +1,226 @@
|
||||
// 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.vm;
|
||||
|
||||
import com.cloud.exception.InvalidParameterValueException;
|
||||
import com.cloud.exception.ManagementServerException;
|
||||
import com.cloud.exception.ResourceUnavailableException;
|
||||
import com.cloud.exception.VirtualMachineMigrationException;
|
||||
import com.cloud.host.Host;
|
||||
import com.cloud.resource.ResourceService;
|
||||
import com.cloud.uservm.UserVm;
|
||||
import com.cloud.utils.db.UUIDManager;
|
||||
import com.cloud.vm.UserVmService;
|
||||
import com.cloud.vm.VirtualMachine;
|
||||
import org.apache.cloudstack.api.ApiConstants;
|
||||
import org.apache.cloudstack.api.ResponseGenerator;
|
||||
import org.apache.cloudstack.api.ResponseObject;
|
||||
import org.apache.cloudstack.api.ServerApiException;
|
||||
import org.apache.cloudstack.api.response.SystemVmResponse;
|
||||
import org.apache.cloudstack.api.response.UserVmResponse;
|
||||
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.powermock.modules.junit4.PowerMockRunner;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RunWith(PowerMockRunner.class)
|
||||
public class MigrateVirtualMachineWithVolumeCmdTest {
|
||||
@Mock
|
||||
UserVmService userVmServiceMock;
|
||||
|
||||
@Mock
|
||||
UUIDManager uuidManagerMock;
|
||||
|
||||
@Mock
|
||||
ResourceService resourceServiceMock;
|
||||
|
||||
@Mock
|
||||
ResponseGenerator responseGeneratorMock;
|
||||
|
||||
@Mock
|
||||
VirtualMachine virtualMachineMock;
|
||||
|
||||
@Mock
|
||||
Host hostMock;
|
||||
|
||||
@Spy
|
||||
@InjectMocks
|
||||
MigrateVirtualMachineWithVolumeCmd cmdSpy = new MigrateVirtualMachineWithVolumeCmd();
|
||||
|
||||
private Long hostId = 1L;
|
||||
private Long virtualMachineUuid = 1L;
|
||||
private String virtualMachineName = "VM-name";
|
||||
private Map<String, String> migrateVolumeTo = Map.of("key","value");
|
||||
private SystemVmResponse systemVmResponse = new SystemVmResponse();
|
||||
private UserVmResponse userVmResponse = new UserVmResponse();
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
Mockito.when(cmdSpy.getVirtualMachineId()).thenReturn(virtualMachineUuid);
|
||||
Mockito.when(cmdSpy.getHostId()).thenReturn(hostId);
|
||||
Mockito.when(cmdSpy.getVolumeToPool()).thenReturn(migrateVolumeTo);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void executeTestHostIdIsNullAndMigrateVolumeToIsNullThrowsInvalidParameterValueException(){
|
||||
ReflectionTestUtils.setField(cmdSpy, "hostId", null);
|
||||
ReflectionTestUtils.setField(cmdSpy, "migrateVolumeTo", null);
|
||||
|
||||
try {
|
||||
cmdSpy.execute();
|
||||
} catch (Exception e) {
|
||||
Assert.assertEquals(InvalidParameterValueException.class, e.getClass());
|
||||
String expected = String.format("Either %s or %s must be passed for migrating the VM.", ApiConstants.HOST_ID, ApiConstants.MIGRATE_TO);
|
||||
Assert.assertEquals(expected , e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void executeTestVMIsStoppedAndHostIdIsNotNullThrowsInvalidParameterValueException(){
|
||||
ReflectionTestUtils.setField(cmdSpy, "hostId", hostId);
|
||||
ReflectionTestUtils.setField(cmdSpy, "migrateVolumeTo", migrateVolumeTo);
|
||||
|
||||
Mockito.when(userVmServiceMock.getVm(Mockito.anyLong())).thenReturn(virtualMachineMock);
|
||||
Mockito.when(virtualMachineMock.getState()).thenReturn(VirtualMachine.State.Stopped);
|
||||
Mockito.when(virtualMachineMock.toString()).thenReturn(String.format("VM [uuid: %s, name: %s]", virtualMachineUuid, virtualMachineName));
|
||||
|
||||
try {
|
||||
cmdSpy.execute();
|
||||
} catch (Exception e) {
|
||||
Assert.assertEquals(InvalidParameterValueException.class, e.getClass());
|
||||
String expected = String.format("%s is not in the Running state to migrate it to the new host.", virtualMachineMock);
|
||||
Assert.assertEquals(expected , e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void executeTestVMIsRunningAndHostIdIsNullThrowsInvalidParameterValueException(){
|
||||
ReflectionTestUtils.setField(cmdSpy, "hostId", null);
|
||||
ReflectionTestUtils.setField(cmdSpy, "migrateVolumeTo", migrateVolumeTo);
|
||||
|
||||
Mockito.when(userVmServiceMock.getVm(Mockito.anyLong())).thenReturn(virtualMachineMock);
|
||||
Mockito.when(virtualMachineMock.getState()).thenReturn(VirtualMachine.State.Running);
|
||||
Mockito.when(virtualMachineMock.toString()).thenReturn(String.format("VM [uuid: %s, name: %s]", virtualMachineUuid, virtualMachineName));
|
||||
|
||||
try {
|
||||
cmdSpy.execute();
|
||||
} catch (Exception e) {
|
||||
Assert.assertEquals(InvalidParameterValueException.class, e.getClass());
|
||||
String expected = String.format("%s is not in the Stopped state to migrate, use the %s parameter to migrate it to a new host.", virtualMachineMock,
|
||||
ApiConstants.HOST_ID);
|
||||
Assert.assertEquals(expected , e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void executeTestHostIdIsNullThrowsInvalidParameterValueException(){
|
||||
ReflectionTestUtils.setField(cmdSpy, "hostId", hostId);
|
||||
ReflectionTestUtils.setField(cmdSpy, "migrateVolumeTo", migrateVolumeTo);
|
||||
|
||||
Mockito.when(userVmServiceMock.getVm(Mockito.anyLong())).thenReturn(virtualMachineMock);
|
||||
Mockito.when(virtualMachineMock.getState()).thenReturn(VirtualMachine.State.Running);
|
||||
Mockito.when(resourceServiceMock.getHost(Mockito.anyLong())).thenReturn(null);
|
||||
Mockito.when(uuidManagerMock.getUuid(Host.class, virtualMachineUuid)).thenReturn(virtualMachineUuid.toString());
|
||||
|
||||
try {
|
||||
cmdSpy.execute();
|
||||
} catch (Exception e) {
|
||||
Assert.assertEquals(InvalidParameterValueException.class, e.getClass());
|
||||
String expected = "Unable to find the specified host to migrate the VM.";
|
||||
Assert.assertEquals(expected , e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void executeTestHostIsNotNullMigratedVMIsNullThrowsServerApiException() throws ManagementServerException, ResourceUnavailableException, VirtualMachineMigrationException {
|
||||
ReflectionTestUtils.setField(cmdSpy, "hostId", hostId);
|
||||
ReflectionTestUtils.setField(cmdSpy, "migrateVolumeTo", migrateVolumeTo);
|
||||
|
||||
Mockito.when(userVmServiceMock.getVm(Mockito.anyLong())).thenReturn(virtualMachineMock);
|
||||
Mockito.when(virtualMachineMock.getState()).thenReturn(VirtualMachine.State.Running);
|
||||
Mockito.when(resourceServiceMock.getHost(Mockito.anyLong())).thenReturn(hostMock);
|
||||
Mockito.when(userVmServiceMock.migrateVirtualMachineWithVolume(virtualMachineUuid, hostMock, migrateVolumeTo)).thenReturn(null);
|
||||
|
||||
try {
|
||||
cmdSpy.execute();
|
||||
} catch (Exception e) {
|
||||
Assert.assertEquals(ServerApiException.class, e.getClass());
|
||||
String expected = "Failed to migrate vm";
|
||||
Assert.assertEquals(expected , e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void executeTestHostIsNullMigratedVMIsNullThrowsServerApiException() {
|
||||
ReflectionTestUtils.setField(cmdSpy, "hostId", null);
|
||||
ReflectionTestUtils.setField(cmdSpy, "migrateVolumeTo", migrateVolumeTo);
|
||||
|
||||
Mockito.when(userVmServiceMock.getVm(Mockito.anyLong())).thenReturn(virtualMachineMock);
|
||||
Mockito.when(virtualMachineMock.getState()).thenReturn(VirtualMachine.State.Stopped);
|
||||
Mockito.when(userVmServiceMock.vmStorageMigration(virtualMachineUuid, migrateVolumeTo)).thenReturn(null);
|
||||
|
||||
try {
|
||||
cmdSpy.execute();
|
||||
} catch (Exception e) {
|
||||
Assert.assertEquals(ServerApiException.class, e.getClass());
|
||||
String expected = "Failed to migrate vm";
|
||||
Assert.assertEquals(expected , e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void executeTestSystemVMMigratedWithSuccess() {
|
||||
ReflectionTestUtils.setField(cmdSpy, "hostId", null);
|
||||
ReflectionTestUtils.setField(cmdSpy, "migrateVolumeTo", migrateVolumeTo);
|
||||
|
||||
Mockito.when(userVmServiceMock.getVm(Mockito.anyLong())).thenReturn(virtualMachineMock);
|
||||
Mockito.when(virtualMachineMock.getState()).thenReturn(VirtualMachine.State.Stopped);
|
||||
Mockito.when(userVmServiceMock.vmStorageMigration(virtualMachineUuid, migrateVolumeTo)).thenReturn(virtualMachineMock);
|
||||
Mockito.when(virtualMachineMock.getType()).thenReturn(VirtualMachine.Type.ConsoleProxy);
|
||||
Mockito.when(responseGeneratorMock.createSystemVmResponse(virtualMachineMock)).thenReturn(systemVmResponse);
|
||||
|
||||
cmdSpy.execute();
|
||||
|
||||
Mockito.verify(responseGeneratorMock, Mockito.times(1)).createSystemVmResponse(virtualMachineMock);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void executeTestUserVMMigratedWithSuccess() {
|
||||
UserVm userVmMock = Mockito.mock(UserVm.class);
|
||||
ReflectionTestUtils.setField(cmdSpy, "hostId", null);
|
||||
ReflectionTestUtils.setField(cmdSpy, "migrateVolumeTo", migrateVolumeTo);
|
||||
|
||||
Mockito.when(userVmServiceMock.getVm(Mockito.anyLong())).thenReturn(userVmMock);
|
||||
Mockito.when(userVmMock.getState()).thenReturn(VirtualMachine.State.Stopped);
|
||||
Mockito.when(userVmServiceMock.vmStorageMigration(virtualMachineUuid, migrateVolumeTo)).thenReturn(userVmMock);
|
||||
Mockito.when(userVmMock.getType()).thenReturn(VirtualMachine.Type.User);
|
||||
Mockito.when(responseGeneratorMock.createUserVmResponse(ResponseObject.ResponseView.Full, "virtualmachine", userVmMock)).thenReturn(List.of(userVmResponse));
|
||||
|
||||
cmdSpy.execute();
|
||||
|
||||
Mockito.verify(responseGeneratorMock, Mockito.times(1)).createUserVmResponse(ResponseObject.ResponseView.Full, "virtualmachine", userVmMock);
|
||||
}
|
||||
}
|
||||
@ -2082,6 +2082,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q
|
||||
Long diskOffId = cmd.getDiskOfferingId();
|
||||
Boolean display = cmd.getDisplay();
|
||||
String state = cmd.getState();
|
||||
boolean shouldListSystemVms = shouldListSystemVms(cmd, caller.getId());
|
||||
|
||||
Long zoneId = cmd.getZoneId();
|
||||
Long podId = cmd.getPodId();
|
||||
@ -2126,14 +2127,16 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q
|
||||
sb.and("display", sb.entity().isDisplayVolume(), SearchCriteria.Op.EQ);
|
||||
sb.and("state", sb.entity().getState(), SearchCriteria.Op.EQ);
|
||||
sb.and("stateNEQ", sb.entity().getState(), SearchCriteria.Op.NEQ);
|
||||
sb.and().op("systemUse", sb.entity().isSystemUse(), SearchCriteria.Op.NEQ);
|
||||
sb.or("nulltype", sb.entity().isSystemUse(), SearchCriteria.Op.NULL);
|
||||
sb.cp();
|
||||
|
||||
// display UserVM volumes only
|
||||
sb.and().op("type", sb.entity().getVmType(), SearchCriteria.Op.NIN);
|
||||
sb.or("nulltype", sb.entity().getVmType(), SearchCriteria.Op.NULL);
|
||||
sb.cp();
|
||||
if (!shouldListSystemVms) {
|
||||
sb.and().op("systemUse", sb.entity().isSystemUse(), SearchCriteria.Op.NEQ);
|
||||
sb.or("nulltype", sb.entity().isSystemUse(), SearchCriteria.Op.NULL);
|
||||
sb.cp();
|
||||
|
||||
sb.and().op("type", sb.entity().getVmType(), SearchCriteria.Op.NIN);
|
||||
sb.or("nulltype", sb.entity().getVmType(), SearchCriteria.Op.NULL);
|
||||
sb.cp();
|
||||
}
|
||||
|
||||
// now set the SC criteria...
|
||||
SearchCriteria<VolumeJoinVO> sc = sb.create();
|
||||
@ -2158,7 +2161,10 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q
|
||||
|
||||
setIdsListToSearchCriteria(sc, ids);
|
||||
|
||||
sc.setParameters("systemUse", 1);
|
||||
if (!shouldListSystemVms) {
|
||||
sc.setParameters("systemUse", 1);
|
||||
sc.setParameters("type", VirtualMachine.Type.ConsoleProxy, VirtualMachine.Type.SecondaryStorageVm, VirtualMachine.Type.DomainRouter);
|
||||
}
|
||||
|
||||
if (tags != null && !tags.isEmpty()) {
|
||||
SearchCriteria<VolumeJoinVO> tagSc = _volumeJoinDao.createSearchCriteria();
|
||||
@ -2206,8 +2212,6 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q
|
||||
if (clusterId != null) {
|
||||
sc.setParameters("clusterId", clusterId);
|
||||
}
|
||||
// Don't return DomR and ConsoleProxy volumes
|
||||
sc.setParameters("type", VirtualMachine.Type.ConsoleProxy, VirtualMachine.Type.SecondaryStorageVm, VirtualMachine.Type.DomainRouter);
|
||||
|
||||
if (state != null) {
|
||||
sc.setParameters("state", state);
|
||||
@ -2232,6 +2236,10 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q
|
||||
return new Pair<List<VolumeJoinVO>, Integer>(vrs, count);
|
||||
}
|
||||
|
||||
private boolean shouldListSystemVms(ListVolumesCmd cmd, Long callerId) {
|
||||
return Boolean.TRUE.equals(cmd.getListSystemVms()) && _accountMgr.isRootAdmin(callerId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ListResponse<DomainResponse> searchForDomains(ListDomainsCmd cmd) {
|
||||
Pair<List<DomainJoinVO>, Integer> result = searchForDomainsInternal(cmd);
|
||||
|
||||
@ -98,6 +98,10 @@ public class VolumeJoinDaoImpl extends GenericDaoBaseWithTagInformation<VolumeJo
|
||||
volResponse.setPodName(volume.getPodName());
|
||||
}
|
||||
|
||||
if (volume.getVmType() != null) {
|
||||
volResponse.setVmType(volume.getVmType().toString());
|
||||
}
|
||||
|
||||
if (volume.getVolumeType() != null) {
|
||||
volResponse.setVolumeType(volume.getVolumeType().toString());
|
||||
}
|
||||
|
||||
@ -6288,6 +6288,11 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
|
||||
return _vmDao.findById(vmId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public VirtualMachine getVm(long vmId) {
|
||||
return _vmInstanceDao.findById(vmId);
|
||||
}
|
||||
|
||||
private VMInstanceVO preVmStorageMigrationCheck(Long vmId) {
|
||||
// access check - only root admin can migrate VM
|
||||
Account caller = CallContext.current().getCallingAccount();
|
||||
|
||||
@ -370,7 +370,7 @@
|
||||
<div class="resource-detail-item__label">{{ $t('label.vmname') }}</div>
|
||||
<div class="resource-detail-item__details">
|
||||
<desktop-outlined />
|
||||
<router-link :to="{ path: '/vm/' + resource.virtualmachineid }">{{ resource.vmname || resource.vm || resource.virtualmachinename || resource.virtualmachineid }} </router-link>
|
||||
<router-link :to="{ path: createPathBasedOnVmType(resource.vmtype, resource.virtualmachineid) }">{{ resource.vmname || resource.vm || resource.virtualmachinename || resource.virtualmachineid }} </router-link>
|
||||
<status class="status status--end" :text="resource.vmstate" v-if="resource.vmstate"/>
|
||||
</div>
|
||||
</div>
|
||||
@ -705,6 +705,7 @@
|
||||
|
||||
<script>
|
||||
import { api } from '@/api'
|
||||
import { createPathBasedOnVmType } from '@/utils/plugins'
|
||||
import Console from '@/components/widgets/Console'
|
||||
import OsLogo from '@/components/widgets/OsLogo'
|
||||
import Status from '@/components/widgets/Status'
|
||||
@ -837,6 +838,7 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
createPathBasedOnVmType: createPathBasedOnVmType,
|
||||
updateResourceAdditionalData () {
|
||||
if (!this.resource) return
|
||||
this.resourceType = this.$route.meta.resourceType
|
||||
|
||||
@ -168,7 +168,7 @@
|
||||
{{ text }}
|
||||
</template>
|
||||
<template #vmname="{ text, record }">
|
||||
<router-link :to="{ path: '/vm/' + record.virtualmachineid }">{{ text }}</router-link>
|
||||
<router-link :to="{ path: createPathBasedOnVmType(record.vmtype, record.virtualmachineid) }">{{ text }}</router-link>
|
||||
</template>
|
||||
<template #virtualmachinename="{ text, record }">
|
||||
<router-link :to="{ path: '/vm/' + record.virtualmachineid }">{{ text }}</router-link>
|
||||
@ -419,6 +419,7 @@ import QuickView from '@/components/view/QuickView'
|
||||
import TooltipButton from '@/components/widgets/TooltipButton'
|
||||
import ResourceIcon from '@/components/view/ResourceIcon'
|
||||
import ResourceLabel from '@/components/widgets/ResourceLabel'
|
||||
import { createPathBasedOnVmType } from '@/utils/plugins'
|
||||
|
||||
export default {
|
||||
name: 'ListView',
|
||||
@ -513,6 +514,7 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
createPathBasedOnVmType: createPathBasedOnVmType,
|
||||
quickViewEnabled () {
|
||||
return new RegExp(['/vm', '/kubernetes', '/ssh', '/userdata', '/vmgroup', '/affinitygroup',
|
||||
'/volume', '/snapshot', '/vmsnapshot', '/backup',
|
||||
|
||||
125
ui/src/components/view/VolumesTab.vue
Normal file
125
ui/src/components/view/VolumesTab.vue
Normal file
@ -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.
|
||||
|
||||
<template>
|
||||
<a-table
|
||||
class="table"
|
||||
size="small"
|
||||
:columns="volumeColumns"
|
||||
:dataSource="volumes"
|
||||
:rowKey="item => item.id"
|
||||
:pagination="false"
|
||||
>
|
||||
<template #name="{ text, record }">
|
||||
<hdd-outlined style="margin-right: 5px"/>
|
||||
<router-link :to="{ path: '/volume/' + record.id }" style="margin-right: 5px">
|
||||
{{ text }}
|
||||
</router-link>
|
||||
<a-tag v-if="record.provisioningtype">
|
||||
{{ record.provisioningtype }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template #state="{ text }">
|
||||
<status :text="text ? text : ''" />{{ text }}
|
||||
</template>
|
||||
<template #size="{ record }">
|
||||
{{ parseFloat(record.size / (1024.0 * 1024.0 * 1024.0)).toFixed(2) }} GB
|
||||
</template>
|
||||
</a-table>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { api } from '@/api'
|
||||
import Status from '@/components/widgets/Status'
|
||||
|
||||
export default {
|
||||
name: 'VolumesTab',
|
||||
components: {
|
||||
Status
|
||||
},
|
||||
props: {
|
||||
resource: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
items: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
inject: ['parentFetchData'],
|
||||
data () {
|
||||
return {
|
||||
vm: {},
|
||||
volumes: [],
|
||||
volumeColumns: [
|
||||
{
|
||||
title: this.$t('label.name'),
|
||||
dataIndex: 'name',
|
||||
slots: { customRender: 'name' }
|
||||
},
|
||||
{
|
||||
title: this.$t('label.state'),
|
||||
dataIndex: 'state',
|
||||
slots: { customRender: 'state' }
|
||||
},
|
||||
{
|
||||
title: this.$t('label.type'),
|
||||
dataIndex: 'type'
|
||||
},
|
||||
{
|
||||
title: this.$t('label.size'),
|
||||
dataIndex: 'size',
|
||||
slots: { customRender: 'size' }
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.vm = this.resource
|
||||
this.fetchData()
|
||||
console.log(this.resource.volumes)
|
||||
},
|
||||
watch: {
|
||||
resource: function (newItem) {
|
||||
this.vm = newItem
|
||||
this.fetchData()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
fetchData () {
|
||||
this.volumes = []
|
||||
if (!this.vm?.id) {
|
||||
return
|
||||
}
|
||||
if (this.items.length) {
|
||||
this.volumes = this.items
|
||||
} else {
|
||||
this.getVolumes()
|
||||
}
|
||||
},
|
||||
getVolumes () {
|
||||
api('listVolumes', { listall: true, listsystemvms: true, virtualmachineid: this.vm.id }).then(json => {
|
||||
this.volumes = json.listvolumesresponse.volume
|
||||
if (this.volumes) {
|
||||
this.volumes.sort((a, b) => { return a.deviceid - b.deviceid })
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@ -43,6 +43,9 @@ export default {
|
||||
name: 'router.health.checks',
|
||||
show: (record, route, user) => { return ['Running'].includes(record.state) && ['Admin'].includes(user.roletype) },
|
||||
component: shallowRef(defineAsyncComponent(() => import('@views/infra/routers/RouterHealthCheck.vue')))
|
||||
}, {
|
||||
name: 'volume',
|
||||
component: shallowRef(defineAsyncComponent(() => import('@/components/view/VolumesTab.vue')))
|
||||
}, {
|
||||
name: 'events',
|
||||
resourceType: 'DomainRouter',
|
||||
|
||||
@ -32,6 +32,10 @@ export default {
|
||||
name: 'details',
|
||||
component: shallowRef(defineAsyncComponent(() => import('@/components/view/DetailsTab.vue')))
|
||||
},
|
||||
{
|
||||
name: 'volume',
|
||||
component: shallowRef(defineAsyncComponent(() => import('@/components/view/VolumesTab.vue')))
|
||||
},
|
||||
{
|
||||
name: 'events',
|
||||
resourceType: 'SystemVm',
|
||||
|
||||
@ -485,3 +485,20 @@ export const genericUtilPlugin = {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function createPathBasedOnVmType (vmtype, virtualmachineid) {
|
||||
let path = ''
|
||||
switch (vmtype) {
|
||||
case 'ConsoleProxy':
|
||||
case 'SecondaryStorageVm':
|
||||
path = '/systemvm/'
|
||||
break
|
||||
case 'DomainRouter':
|
||||
path = '/router/'
|
||||
break
|
||||
default:
|
||||
path = '/vm/'
|
||||
}
|
||||
|
||||
return path + virtualmachineid
|
||||
}
|
||||
|
||||
@ -847,6 +847,10 @@ export default {
|
||||
delete params.showunique
|
||||
}
|
||||
|
||||
if (['Admin'].includes(this.$store.getters.userInfo.roletype) && ['listVolumesMetrics', 'listVolumes'].includes(this.apiName)) {
|
||||
params.listsystemvms = true
|
||||
}
|
||||
|
||||
this.loading = true
|
||||
if (this.$route.params && this.$route.params.id) {
|
||||
params.id = this.$route.params.id
|
||||
|
||||
@ -33,31 +33,8 @@
|
||||
<router-link :to="{ path: '/iso/' + vm.isoid }">{{ vm.isoname }}</router-link> <br/>
|
||||
<barcode-outlined /> {{ vm.isoid }}
|
||||
</a-tab-pane>
|
||||
<a-tab-pane :tab="$t('label.volumes')" key="volumes" v-if="'listVolumes' in $store.getters.apis">
|
||||
<a-table
|
||||
class="table"
|
||||
size="small"
|
||||
:columns="volumeColumns"
|
||||
:dataSource="volumes"
|
||||
:rowKey="item => item.id"
|
||||
:pagination="false"
|
||||
>
|
||||
<template #name="{ text, record }">
|
||||
<hdd-outlined />
|
||||
<router-link :to="{ path: '/volume/' + record.id }">
|
||||
{{ text }}
|
||||
</router-link>
|
||||
<a-tag v-if="record.provisioningtype">
|
||||
{{ record.provisioningtype }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template #state="{ text }">
|
||||
<status :text="text ? text : ''" />{{ text }}
|
||||
</template>
|
||||
<template #size="{ record }">
|
||||
{{ parseFloat(record.size / (1024.0 * 1024.0 * 1024.0)).toFixed(2) }} GB
|
||||
</template>
|
||||
</a-table>
|
||||
<a-tab-pane :tab="$t('label.volumes')" key="volumes">
|
||||
<volumes-tab :resource="vm" :items="volumes" :loading="loading" />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane :tab="$t('label.nics')" key="nics" v-if="'listNics' in $store.getters.apis">
|
||||
<a-button
|
||||
@ -308,7 +285,6 @@
|
||||
import { api } from '@/api'
|
||||
import { mixinDevice } from '@/utils/mixin.js'
|
||||
import ResourceLayout from '@/layouts/ResourceLayout'
|
||||
import Status from '@/components/widgets/Status'
|
||||
import DetailsTab from '@/components/view/DetailsTab'
|
||||
import StatsTab from '@/components/view/StatsTab'
|
||||
import EventsTab from '@/components/view/EventsTab'
|
||||
@ -318,6 +294,7 @@ import ListResourceTable from '@/components/view/ListResourceTable'
|
||||
import TooltipButton from '@/components/widgets/TooltipButton'
|
||||
import ResourceIcon from '@/components/view/ResourceIcon'
|
||||
import AnnotationsTab from '@/components/view/AnnotationsTab'
|
||||
import VolumesTab from '@/components/view/VolumesTab.vue'
|
||||
|
||||
export default {
|
||||
name: 'InstanceTab',
|
||||
@ -328,11 +305,11 @@ export default {
|
||||
EventsTab,
|
||||
DetailSettings,
|
||||
NicsTable,
|
||||
Status,
|
||||
ListResourceTable,
|
||||
TooltipButton,
|
||||
ResourceIcon,
|
||||
AnnotationsTab
|
||||
AnnotationsTab,
|
||||
VolumesTab
|
||||
},
|
||||
mixins: [mixinDevice],
|
||||
props: {
|
||||
@ -349,7 +326,6 @@ export default {
|
||||
data () {
|
||||
return {
|
||||
vm: {},
|
||||
volumes: [],
|
||||
totalStorage: 0,
|
||||
currentTab: 'details',
|
||||
showAddNetworkModal: false,
|
||||
@ -367,27 +343,6 @@ export default {
|
||||
secondaryIPs: [],
|
||||
selectedNicId: '',
|
||||
newSecondaryIp: '',
|
||||
volumeColumns: [
|
||||
{
|
||||
title: this.$t('label.name'),
|
||||
dataIndex: 'name',
|
||||
slots: { customRender: 'name' }
|
||||
},
|
||||
{
|
||||
title: this.$t('label.state'),
|
||||
dataIndex: 'state',
|
||||
slots: { customRender: 'state' }
|
||||
},
|
||||
{
|
||||
title: this.$t('label.type'),
|
||||
dataIndex: 'type'
|
||||
},
|
||||
{
|
||||
title: this.$t('label.size'),
|
||||
dataIndex: 'size',
|
||||
slots: { customRender: 'size' }
|
||||
}
|
||||
],
|
||||
editNicResource: {},
|
||||
listIps: {
|
||||
loading: false,
|
||||
@ -443,18 +398,10 @@ export default {
|
||||
)
|
||||
},
|
||||
fetchData () {
|
||||
this.volumes = []
|
||||
this.annotations = []
|
||||
if (!this.vm || !this.vm.id) {
|
||||
return
|
||||
}
|
||||
api('listVolumes', { listall: true, virtualmachineid: this.vm.id }).then(json => {
|
||||
this.volumes = json.listvolumesresponse.volume
|
||||
if (this.volumes) {
|
||||
this.volumes.sort((a, b) => { return a.deviceid - b.deviceid })
|
||||
}
|
||||
this.dataResource.volumes = this.volumes
|
||||
})
|
||||
api('listAnnotations', { entityid: this.dataResource.id, entitytype: 'VM', annotationfilter: 'all' }).then(json => {
|
||||
if (json.listannotationsresponse && json.listannotationsresponse.annotation) {
|
||||
this.annotations = json.listannotationsresponse.annotation
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user