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:
Bryan Lima 2022-10-28 08:14:09 -03:00 committed by GitHub
parent f580a8d7a2
commit 23033fbb74
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 464 additions and 84 deletions

View File

@ -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

View File

@ -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";

View File

@ -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);
}
}

View File

@ -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) {

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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);

View File

@ -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());
}

View File

@ -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();

View File

@ -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

View File

@ -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',

View 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>

View File

@ -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',

View File

@ -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',

View File

@ -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
}

View File

@ -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

View File

@ -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