Destroyvm also removes volumes (#2793)

* Allow user to detach and delete volumes when destroyinh VMs

* Minor code refactoring
This commit is contained in:
Henko 2018-11-30 15:27:31 +02:00 committed by Nicolas Vazquez
parent 9a4149e5dc
commit 525ddfb717
10 changed files with 271 additions and 10 deletions

View File

@ -79,6 +79,8 @@ public interface VolumeApiService {
Volume attachVolumeToVM(AttachVolumeCmd command); Volume attachVolumeToVM(AttachVolumeCmd command);
Volume detachVolumeViaDestroyVM(long vmId, long volumeId);
Volume detachVolumeFromVM(DetachVolumeCmd cmd); Volume detachVolumeFromVM(DetachVolumeCmd cmd);
Snapshot takeSnapshot(Long volumeId, Long policyId, Long snapshotId, Account account, boolean quiescevm, Snapshot.LocationType locationType, boolean asyncBackup) Snapshot takeSnapshot(Long volumeId, Long policyId, Long snapshotId, Account account, boolean quiescevm, Snapshot.LocationType locationType, boolean asyncBackup)

View File

@ -714,6 +714,7 @@ public class ApiConstants {
public static final String STDERR = "stderr"; public static final String STDERR = "stderr";
public static final String EXITCODE = "exitcode"; public static final String EXITCODE = "exitcode";
public static final String TARGET_ID = "targetid"; public static final String TARGET_ID = "targetid";
public static final String VOLUME_IDS = "volumeids";
public enum HostDetails { public enum HostDetails {
all, capacity, events, stats, min; all, capacity, events, stats, min;

View File

@ -31,6 +31,7 @@ import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.ResponseObject.ResponseView; import org.apache.cloudstack.api.ResponseObject.ResponseView;
import org.apache.cloudstack.api.ServerApiException; import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.response.UserVmResponse; import org.apache.cloudstack.api.response.UserVmResponse;
import org.apache.cloudstack.api.response.VolumeResponse;
import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.context.CallContext;
import com.cloud.event.EventTypes; import com.cloud.event.EventTypes;
@ -63,6 +64,14 @@ public class DestroyVMCmd extends BaseAsyncCmd {
since = "4.2.1") since = "4.2.1")
private Boolean expunge; private Boolean expunge;
@Parameter( name = ApiConstants.VOLUME_IDS,
type = CommandType.LIST,
collectionType = CommandType.UUID,
entityType = VolumeResponse.class,
description = "Comma separated list of UUIDs for volumes that will be deleted",
since = "4.12.0")
private List<Long> volumeIds;
///////////////////////////////////////////////////// /////////////////////////////////////////////////////
/////////////////// Accessors /////////////////////// /////////////////// Accessors ///////////////////////
///////////////////////////////////////////////////// /////////////////////////////////////////////////////
@ -78,6 +87,10 @@ public class DestroyVMCmd extends BaseAsyncCmd {
return expunge; return expunge;
} }
public List<Long> getVolumeIds() {
return volumeIds;
}
///////////////////////////////////////////////////// /////////////////////////////////////////////////////
/////////////// API Implementation/////////////////// /////////////// API Implementation///////////////////
///////////////////////////////////////////////////// /////////////////////////////////////////////////////

View File

@ -1857,8 +1857,12 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic
} }
} }
private Volume orchestrateDetachVolumeFromVM(long vmId, long volumeId) { @ActionEvent(eventType = EventTypes.EVENT_VOLUME_DETACH, eventDescription = "detaching volume")
public Volume detachVolumeViaDestroyVM(long vmId, long volumeId) {
return orchestrateDetachVolumeFromVM(vmId, volumeId);
}
private Volume orchestrateDetachVolumeFromVM(long vmId, long volumeId) {
Volume volume = _volsDao.findById(volumeId); Volume volume = _volsDao.findById(volumeId);
VMInstanceVO vm = _vmInstanceDao.findById(vmId); VMInstanceVO vm = _vmInstanceDao.findById(vmId);
@ -1869,7 +1873,6 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic
if (hostId == null) { if (hostId == null) {
hostId = vm.getLastHostId(); hostId = vm.getLastHostId();
HostVO host = _hostDao.findById(hostId); HostVO host = _hostDao.findById(hostId);
if (host != null && host.getHypervisorType() == HypervisorType.VMware) { if (host != null && host.getHypervisorType() == HypervisorType.VMware) {

View File

@ -39,12 +39,6 @@ import java.util.stream.Collectors;
import javax.inject.Inject; import javax.inject.Inject;
import javax.naming.ConfigurationException; import javax.naming.ConfigurationException;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.Logger;
import com.cloud.user.AccountVO;
import org.apache.cloudstack.acl.ControlledEntity.ACLType; import org.apache.cloudstack.acl.ControlledEntity.ACLType;
import org.apache.cloudstack.acl.SecurityChecker.AccessType; import org.apache.cloudstack.acl.SecurityChecker.AccessType;
import org.apache.cloudstack.affinity.AffinityGroupService; import org.apache.cloudstack.affinity.AffinityGroupService;
@ -97,6 +91,10 @@ import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao;
import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreDao; import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreDao;
import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreVO; import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreVO;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.Logger;
import com.cloud.agent.AgentManager; import com.cloud.agent.AgentManager;
import com.cloud.agent.api.Answer; import com.cloud.agent.api.Answer;
@ -256,6 +254,7 @@ import com.cloud.template.VirtualMachineTemplate;
import com.cloud.user.Account; import com.cloud.user.Account;
import com.cloud.user.AccountManager; import com.cloud.user.AccountManager;
import com.cloud.user.AccountService; import com.cloud.user.AccountService;
import com.cloud.user.AccountVO;
import com.cloud.user.ResourceLimitService; import com.cloud.user.ResourceLimitService;
import com.cloud.user.SSHKeyPair; import com.cloud.user.SSHKeyPair;
import com.cloud.user.SSHKeyPairVO; import com.cloud.user.SSHKeyPairVO;
@ -517,6 +516,9 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
private static final ConfigKey<Boolean> EnableAdditionalVmConfig = new ConfigKey<>("Advanced", Boolean.class, "enable.additional.vm.configuration", private static final ConfigKey<Boolean> EnableAdditionalVmConfig = new ConfigKey<>("Advanced", Boolean.class, "enable.additional.vm.configuration",
"false", "allow additional arbitrary configuration to vm", true, ConfigKey.Scope.Account); "false", "allow additional arbitrary configuration to vm", true, ConfigKey.Scope.Account);
private static final ConfigKey<Boolean> VmDestroyForcestop = new ConfigKey<Boolean>("Advanced", Boolean.class, "vm.destroy.forcestop", "false",
"On destroy, force-stop takes this value ", true);
@Override @Override
public UserVmVO getVirtualMachine(long vmId) { public UserVmVO getVirtualMachine(long vmId) {
@ -2757,10 +2759,15 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
// check if VM exists // check if VM exists
UserVmVO vm = _vmDao.findById(vmId); UserVmVO vm = _vmDao.findById(vmId);
if (vm == null) { if (vm == null || vm.getRemoved() != null) {
throw new InvalidParameterValueException("unable to find a virtual machine with id " + vmId); throw new InvalidParameterValueException("unable to find a virtual machine with id " + vmId);
} }
if (vm.getState() == State.Destroyed || vm.getState() == State.Expunging) {
s_logger.debug("Vm id=" + vmId + " is already destroyed");
return vm;
}
// check if there are active volume snapshots tasks // check if there are active volume snapshots tasks
s_logger.debug("Checking if there are any ongoing snapshots on the ROOT volumes associated with VM with ID " + vmId); s_logger.debug("Checking if there are any ongoing snapshots on the ROOT volumes associated with VM with ID " + vmId);
if (checkStatusOfVolumeSnapshots(vmId, Volume.Type.ROOT)) { if (checkStatusOfVolumeSnapshots(vmId, Volume.Type.ROOT)) {
@ -2768,6 +2775,15 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
} }
s_logger.debug("Found no ongoing snapshots on volume of type ROOT, for the vm with id " + vmId); s_logger.debug("Found no ongoing snapshots on volume of type ROOT, for the vm with id " + vmId);
List<VolumeVO> volumes = getVolumesFromIds(cmd);
checkForUnattachedVolumes(vmId, volumes);
validateVolumes(volumes);
stopVirtualMachine(vmId, VmDestroyForcestop.value());
detachVolumesFromVm(volumes);
UserVm destroyedVm = destroyVm(vmId, expunge); UserVm destroyedVm = destroyVm(vmId, expunge);
if (expunge) { if (expunge) {
if (!expunge(vm, ctx.getCallingUserId(), ctx.getCallingAccount())) { if (!expunge(vm, ctx.getCallingUserId(), ctx.getCallingAccount())) {
@ -2775,9 +2791,26 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
} }
} }
deleteVolumesFromVm(volumes);
return destroyedVm; return destroyedVm;
} }
private List<VolumeVO> getVolumesFromIds(DestroyVMCmd cmd) {
List<VolumeVO> volumes = new ArrayList<>();
if (cmd.getVolumeIds() != null) {
for (Long volId : cmd.getVolumeIds()) {
VolumeVO vol = _volsDao.findById(volId);
if (vol == null) {
throw new InvalidParameterValueException("Unable to find volume with ID: " + volId);
}
volumes.add(vol);
}
}
return volumes;
}
@Override @Override
@DB @DB
public InstanceGroupVO createVmGroup(CreateVMGroupCmd cmd) { public InstanceGroupVO createVmGroup(CreateVMGroupCmd cmd) {
@ -6518,4 +6551,52 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
} }
return false; return false;
} }
private void checkForUnattachedVolumes(long vmId, List<VolumeVO> volumes) {
StringBuilder sb = new StringBuilder();
for (VolumeVO volume : volumes) {
if (volume.getInstanceId() == null || vmId != volume.getInstanceId()) {
sb.append(volume.toString() + "; ");
}
}
if (!StringUtils.isEmpty(sb.toString())) {
throw new InvalidParameterValueException("The following supplied volumes are not attached to the VM: " + sb.toString());
}
}
private void validateVolumes(List<VolumeVO> volumes) {
for (VolumeVO volume : volumes) {
if (!(volume.getVolumeType() == Volume.Type.ROOT || volume.getVolumeType() == Volume.Type.DATADISK)) {
throw new InvalidParameterValueException("Please specify volume of type " + Volume.Type.DATADISK.toString() + " or " + Volume.Type.ROOT.toString());
}
}
}
private void detachVolumesFromVm(List<VolumeVO> volumes) {
for (VolumeVO volume : volumes) {
Volume detachResult = _volumeService.detachVolumeViaDestroyVM(volume.getInstanceId(), volume.getId());
if (detachResult == null) {
s_logger.error("DestroyVM remove volume - failed to detach and delete volume " + volume.getInstanceId() + " from instance " + volume.getId());
}
}
}
private void deleteVolumesFromVm(List<VolumeVO> volumes) {
for (VolumeVO volume : volumes) {
boolean deleteResult = _volumeService.deleteVolume(volume.getId(), CallContext.current().getCallingAccount());
if (!deleteResult) {
s_logger.error("DestroyVM remove volume - failed to delete volume " + volume.getInstanceId() + " from instance " + volume.getId());
}
}
}
} }

View File

@ -32,7 +32,9 @@ from marvin.lib.base import (Account,
Host, Host,
Iso, Iso,
Router, Router,
Configurations) Configurations,
Volume,
DiskOffering)
from marvin.lib.common import (get_domain, from marvin.lib.common import (get_domain,
get_zone, get_zone,
get_template, get_template,
@ -786,6 +788,43 @@ class TestVMLifeCycle(cloudstackTestCase):
) )
return return
@attr(tags = ["devcloud", "advanced", "advancedns", "smoke", "basic", "sg"], required_hardware="false")
def test_11_destroy_vm_and_volumes(self):
"""Test destroy Virtual Machine and it's volumes
"""
# Validate the following
# 1. Deploys a VM and attaches disks to it
# 2. Destroys the VM with DataDisks option
small_disk_offering = DiskOffering.list(self.apiclient, name='Small')[0]
small_virtual_machine = VirtualMachine.create(
self.apiclient,
self.services["small"],
accountid=self.account.name,
domainid=self.account.domainid,
serviceofferingid=self.small_offering.id,
mode=self.services["mode"]
)
vol1 = Volume.create(
self.apiclient,
self.services,
account=self.account.name,
diskofferingid=small_disk_offering.id,
domainid=self.account.domainid,
zoneid=self.zone.id
)
small_virtual_machine.attach_volume(self.apiclient, vol1)
self.debug("Destroy VM - ID: %s" % small_virtual_machine.id)
small_virtual_machine.delete(self.apiclient, volumeIds=vol1.id)
self.assertEqual(VirtualMachine.list(self.apiclient, id=small_virtual_machine.id), None, "List response contains records when it should not")
self.assertEqual(Volume.list(self.apiclient, id=vol1.id), None, "List response contains records when it should not")
class TestSecuredVmMigration(cloudstackTestCase): class TestSecuredVmMigration(cloudstackTestCase):
@classmethod @classmethod

View File

@ -4179,6 +4179,15 @@ textarea {
float: left; float: left;
} }
.ui-dialog div.form-container div.value label {
display: block;
width: 119px;
text-align: left;
font-size: 13px;
margin-top: 2px;
margin-left: -10px;
}
.ui-dialog div.form-container div.value input.hasDatepicker { .ui-dialog div.form-container div.value input.hasDatepicker {
color: #2F5D86; color: #2F5D86;
cursor: pointer; cursor: pointer;

View File

@ -640,6 +640,7 @@ var dictionary = {
"label.delete.secondary.staging.store":"Delete Secondary Staging Store", "label.delete.secondary.staging.store":"Delete Secondary Staging Store",
"label.delete.sslcertificate":"Delete SSL Certificate", "label.delete.sslcertificate":"Delete SSL Certificate",
"label.delete.ucs.manager":"Delete UCS Manager", "label.delete.ucs.manager":"Delete UCS Manager",
"label.delete.volumes":"Volumes to be deleted",
"label.delete.vpn.user":"Delete VPN user", "label.delete.vpn.user":"Delete VPN user",
"label.deleting.failed":"Deleting Failed", "label.deleting.failed":"Deleting Failed",
"label.deleting.processing":"Deleting....", "label.deleting.processing":"Deleting....",
@ -1813,6 +1814,8 @@ var dictionary = {
"label.volgroup":"Volume Group", "label.volgroup":"Volume Group",
"label.volume":"Volume", "label.volume":"Volume",
"label.volume.details":"Volume details", "label.volume.details":"Volume details",
"label.volume.empty":"No volumes attached to this VM",
"label.volume.ids":"Volume ID's",
"label.volume.limits":"Volume Limits", "label.volume.limits":"Volume Limits",
"label.volume.migrated":"Volume migrated", "label.volume.migrated":"Volume migrated",
"label.volume.name":"Volume Name", "label.volume.name":"Volume Name",

View File

@ -112,6 +112,34 @@
label: 'label.expunge', label: 'label.expunge',
isBoolean: true, isBoolean: true,
isChecked: false isChecked: false
},
volumes: {
label: 'label.delete.volumes',
isBoolean: true,
isChecked: false
},
volumeids: {
label: 'label.volume.ids',
dependsOn: 'volumes',
isBoolean: true,
isHidden: true,
emptyMessage: 'label.volume.empty',
multiDataArray: true,
multiData: function(args) {
$.ajax({
url: createURL("listVolumes&virtualMachineId=" + args.context.instances[0].id) + "&type=DATADISK",
dataType: "json",
async: true,
success: function(json) {
var volumes = json.listvolumesresponse.volume;
args.response.success({
descriptionField: 'name',
valueField: 'id',
data: volumes
});
}
});
}
} }
} }
}, },
@ -126,6 +154,26 @@
expunge: true expunge: true
}); });
} }
if (args.data.volumes == 'on') {
var regex = RegExp('[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}');
var selectedVolumes = [];
for (var key in args.data) {
var matches = key.match(regex);
if (matches != null) {
selectedVolumes.push(key);
}
}
$.extend(data, {
volumeids: $(selectedVolumes).map(function(index, volume) {
return volume;
}).toArray().join(',')
});
}
$.ajax({ $.ajax({
url: createURL('destroyVirtualMachine'), url: createURL('destroyVirtualMachine'),
data: data, data: data,

View File

@ -433,6 +433,68 @@
); );
}); });
} else if (field.multiDataArray) {
$input = $('<div>');
multiArgs = {
context: args.context,
response: {
success: function(args) {
if (args.data == undefined || args.data.length == 0) {
var label = field.emptyMessage != null ? field.emptyMessage : 'No data available';
$input
.addClass('value')
.appendTo($value)
.append(
$('<label>').html(_l(label))
);
} else {
$input.addClass('multi-array').addClass(key).appendTo($value);
$(args.data).each(function() {
var id;
if (field.valueField)
id = this[field.valueField];
else
id = this.id !== undefined ? this.id : this.name;
var desc;
if (args.descriptionField)
desc = this[args.descriptionField];
else
desc = _l(this.description);
$input.append(
$('<div>')
.addClass('item')
.append(
$.merge(
$('<div>').addClass('name').html(_l(desc)),
$('<div>').addClass('value').append(
$('<input>').attr({
name: id,
type: 'checkbox'
})
.data('json-obj', this)
.appendTo($value)
)
)
)
);
});
}
}
}
}
multiFn = field.multiData;
multiFn(multiArgs);
} else { } else {
$input = $('<input>').attr({ $input = $('<input>').attr({
name: key, name: key,