Feature: Allow adding delete protection for VMs & volumes (#9633)

Co-authored-by: Suresh Kumar Anaparti <sureshkumar.anaparti@gmail.com>
This commit is contained in:
Vishesh 2024-09-09 18:14:50 +05:30 committed by GitHub
parent f8d8a9c7b3
commit 1303a4f323
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 261 additions and 28 deletions

View File

@ -271,11 +271,13 @@ public interface Volume extends ControlledEntity, Identity, InternalIdentity, Ba
void setExternalUuid(String externalUuid);
public Long getPassphraseId();
Long getPassphraseId();
public void setPassphraseId(Long id);
void setPassphraseId(Long id);
public String getEncryptFormat();
String getEncryptFormat();
public void setEncryptFormat(String encryptFormat);
void setEncryptFormat(String encryptFormat);
boolean isDeleteProtection();
}

View File

@ -117,7 +117,9 @@ public interface VolumeApiService {
Snapshot allocSnapshot(Long volumeId, Long policyId, String snapshotName, Snapshot.LocationType locationType, List<Long> zoneIds) throws ResourceAllocationException;
Volume updateVolume(long volumeId, String path, String state, Long storageId, Boolean displayVolume, String customId, long owner, String chainInfo, String name);
Volume updateVolume(long volumeId, String path, String state, Long storageId,
Boolean displayVolume, Boolean deleteProtection,
String customId, long owner, String chainInfo, String name);
/**
* Extracts the volume to a particular location.

View File

@ -138,6 +138,7 @@ public class ApiConstants {
public static final String DATACENTER_NAME = "datacentername";
public static final String DATADISK_OFFERING_LIST = "datadiskofferinglist";
public static final String DEFAULT_VALUE = "defaultvalue";
public static final String DELETE_PROTECTION = "deleteprotection";
public static final String DESCRIPTION = "description";
public static final String DESTINATION = "destination";
public static final String DESTINATION_ZONE_ID = "destzoneid";

View File

@ -146,6 +146,14 @@ public class UpdateVMCmd extends BaseCustomIdCmd implements SecurityGroupAction,
@Parameter(name = ApiConstants.EXTRA_CONFIG, type = CommandType.STRING, since = "4.12", description = "an optional URL encoded string that can be passed to the virtual machine upon successful deployment", length = 5120)
private String extraConfig;
@Parameter(name = ApiConstants.DELETE_PROTECTION,
type = CommandType.BOOLEAN, since = "4.20.0",
description = "Set delete protection for the virtual machine. If " +
"true, the instance will be protected from deletion. " +
"Note: If the instance is managed by another service like" +
" autoscaling groups or CKS, delete protection will be ignored.")
private Boolean deleteProtection;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
@ -215,6 +223,10 @@ public class UpdateVMCmd extends BaseCustomIdCmd implements SecurityGroupAction,
return cleanupDetails == null ? false : cleanupDetails.booleanValue();
}
public Boolean getDeleteProtection() {
return deleteProtection;
}
public Map<String, Map<Integer, String>> getDhcpOptionsMap() {
Map<String, Map<Integer, String>> dhcpOptionsMap = new HashMap<>();
if (dhcpOptionsNetworkList != null && !dhcpOptionsNetworkList.isEmpty()) {

View File

@ -77,6 +77,14 @@ public class UpdateVolumeCmd extends BaseAsyncCustomIdCmd implements UserCmd {
@Parameter(name = ApiConstants.NAME, type = CommandType.STRING, description = "new name of the volume", since = "4.16")
private String name;
@Parameter(name = ApiConstants.DELETE_PROTECTION,
type = CommandType.BOOLEAN, since = "4.20.0",
description = "Set delete protection for the volume. If true, The volume " +
"will be protected from deletion. Note: If the volume is managed by " +
"another service like autoscaling groups or CKS, delete protection will be " +
"ignored.")
private Boolean deleteProtection;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
@ -109,6 +117,10 @@ public class UpdateVolumeCmd extends BaseAsyncCustomIdCmd implements UserCmd {
return name;
}
public Boolean getDeleteProtection() {
return deleteProtection;
}
/////////////////////////////////////////////////////
/////////////// API Implementation///////////////////
/////////////////////////////////////////////////////
@ -168,7 +180,7 @@ public class UpdateVolumeCmd extends BaseAsyncCustomIdCmd implements UserCmd {
public void execute() {
CallContext.current().setEventDetails("Volume Id: " + this._uuidMgr.getUuid(Volume.class, getId()));
Volume result = _volumeService.updateVolume(getId(), getPath(), getState(), getStorageId(), getDisplayVolume(),
getCustomId(), getEntityOwnerId(), getChainInfo(), getName());
getDeleteProtection(), getCustomId(), getEntityOwnerId(), getChainInfo(), getName());
if (result != null) {
VolumeResponse response = _responseGenerator.createVolumeResponse(getResponseView(), result);
response.setResponseName(getCommandName());

View File

@ -320,6 +320,10 @@ public class UserVmResponse extends BaseResponseWithTagInformation implements Co
@Param(description = "true if vm contains XS/VMWare tools inorder to support dynamic scaling of VM cpu/memory.")
private Boolean isDynamicallyScalable;
@SerializedName(ApiConstants.DELETE_PROTECTION)
@Param(description = "true if vm has delete protection.", since = "4.20.0")
private boolean deleteProtection;
@SerializedName(ApiConstants.SERVICE_STATE)
@Param(description = "State of the Service from LB rule")
private String serviceState;
@ -995,6 +999,14 @@ public class UserVmResponse extends BaseResponseWithTagInformation implements Co
isDynamicallyScalable = dynamicallyScalable;
}
public boolean isDeleteProtection() {
return deleteProtection;
}
public void setDeleteProtection(boolean deleteProtection) {
this.deleteProtection = deleteProtection;
}
public String getOsTypeId() {
return osTypeId;
}

View File

@ -261,6 +261,10 @@ public class VolumeResponse extends BaseResponseWithTagInformation implements Co
@Param(description = "true if storage snapshot is supported for the volume, false otherwise", since = "4.16")
private boolean supportsStorageSnapshot;
@SerializedName(ApiConstants.DELETE_PROTECTION)
@Param(description = "true if volume has delete protection.", since = "4.20.0")
private boolean deleteProtection;
@SerializedName(ApiConstants.PHYSICAL_SIZE)
@Param(description = "the bytes actually consumed on disk")
private Long physicalsize;
@ -584,6 +588,14 @@ public class VolumeResponse extends BaseResponseWithTagInformation implements Co
return this.supportsStorageSnapshot;
}
public boolean isDeleteProtection() {
return deleteProtection;
}
public void setDeleteProtection(boolean deleteProtection) {
this.deleteProtection = deleteProtection;
}
public String getIsoId() {
return isoId;
}

View File

@ -182,6 +182,9 @@ public class VolumeVO implements Volume {
@Column(name = "encrypt_format")
private String encryptFormat;
@Column(name = "delete_protection")
private boolean deleteProtection;
// Real Constructor
public VolumeVO(Type type, String name, long dcId, long domainId,
@ -678,4 +681,13 @@ public class VolumeVO implements Volume {
public String getEncryptFormat() { return encryptFormat; }
public void setEncryptFormat(String encryptFormat) { this.encryptFormat = encryptFormat; }
@Override
public boolean isDeleteProtection() {
return deleteProtection;
}
public void setDeleteProtection(boolean deleteProtection) {
this.deleteProtection = deleteProtection;
}
}

View File

@ -167,10 +167,8 @@ public class VMInstanceVO implements VirtualMachine, FiniteStateObject<State, Vi
@Column(name = "dynamically_scalable")
protected boolean dynamicallyScalable;
/*
@Column(name="tags")
protected String tags;
*/
@Column(name = "delete_protection")
protected boolean deleteProtection;
@Transient
Map<String, String> details;
@ -542,6 +540,14 @@ public class VMInstanceVO implements VirtualMachine, FiniteStateObject<State, Vi
return dynamicallyScalable;
}
public boolean isDeleteProtection() {
return deleteProtection;
}
public void setDeleteProtection(boolean deleteProtection) {
this.deleteProtection = deleteProtection;
}
@Override
public Class<?> getEntityType() {
return VirtualMachine.class;

View File

@ -53,7 +53,11 @@ public interface UserVmDao extends GenericDao<UserVmVO, Long> {
* @param hostName TODO
* @param instanceName
*/
void updateVM(long id, String displayName, boolean enable, Long osTypeId, String userData, Long userDataId, String userDataDetails, boolean displayVm, boolean isDynamicallyScalable, String customId, String hostName, String instanceName);
void updateVM(long id, String displayName, boolean enable, Long osTypeId,
String userData, Long userDataId, String userDataDetails,
boolean displayVm, boolean isDynamicallyScalable,
boolean deleteProtection, String customId, String hostName,
String instanceName);
List<UserVmVO> findDestroyedVms(Date date);

View File

@ -274,8 +274,11 @@ public class UserVmDaoImpl extends GenericDaoBase<UserVmVO, Long> implements Use
}
@Override
public void updateVM(long id, String displayName, boolean enable, Long osTypeId, String userData, Long userDataId, String userDataDetails, boolean displayVm,
boolean isDynamicallyScalable, String customId, String hostName, String instanceName) {
public void updateVM(long id, String displayName, boolean enable, Long osTypeId,
String userData, Long userDataId, String userDataDetails,
boolean displayVm, boolean isDynamicallyScalable,
boolean deleteProtection, String customId, String hostName,
String instanceName) {
UserVmVO vo = createForUpdate();
vo.setDisplayName(displayName);
vo.setHaEnabled(enable);
@ -285,6 +288,7 @@ public class UserVmDaoImpl extends GenericDaoBase<UserVmVO, Long> implements Use
vo.setUserDataDetails(userDataDetails);
vo.setDisplayVm(displayVm);
vo.setDynamicallyScalable(isDynamicallyScalable);
vo.setDeleteProtection(deleteProtection);
if (hostName != null) {
vo.setHostName(hostName);
}

View File

@ -620,3 +620,6 @@ INSERT IGNORE INTO `cloud`.`hypervisor_capabilities` (uuid, hypervisor_type, hyp
INSERT IGNORE INTO `cloud`.`guest_os_hypervisor` (uuid, hypervisor_type, hypervisor_version, guest_os_name, guest_os_id, created, is_user_defined) SELECT UUID(),'VMware', '8.0.2', guest_os_name, guest_os_id, utc_timestamp(), 0 FROM `cloud`.`guest_os_hypervisor` WHERE hypervisor_type='VMware' AND hypervisor_version='8.0';
INSERT IGNORE INTO `cloud`.`hypervisor_capabilities` (uuid, hypervisor_type, hypervisor_version, max_guests_limit, security_group_enabled, max_data_volumes_limit, max_hosts_per_cluster, storage_motion_supported, vm_snapshot_enabled) values (UUID(), 'VMware', '8.0.3', 1024, 0, 59, 64, 1, 1);
INSERT IGNORE INTO `cloud`.`guest_os_hypervisor` (uuid, hypervisor_type, hypervisor_version, guest_os_name, guest_os_id, created, is_user_defined) SELECT UUID(),'VMware', '8.0.3', guest_os_name, guest_os_id, utc_timestamp(), 0 FROM `cloud`.`guest_os_hypervisor` WHERE hypervisor_type='VMware' AND hypervisor_version='8.0';
CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.vm_instance', 'delete_protection', 'boolean DEFAULT FALSE COMMENT "delete protection for vm" ');
CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.volumes', 'delete_protection', 'boolean DEFAULT FALSE COMMENT "delete protection for volumes" ');

View File

@ -54,6 +54,7 @@ SELECT
`vm_instance`.`instance_name` AS `instance_name`,
`vm_instance`.`guest_os_id` AS `guest_os_id`,
`vm_instance`.`display_vm` AS `display_vm`,
`vm_instance`.`delete_protection` AS `delete_protection`,
`guest_os`.`uuid` AS `guest_os_uuid`,
`vm_instance`.`pod_id` AS `pod_id`,
`host_pod_ref`.`uuid` AS `pod_uuid`,

View File

@ -40,6 +40,7 @@ SELECT
`volumes`.`chain_info` AS `chain_info`,
`volumes`.`external_uuid` AS `external_uuid`,
`volumes`.`encrypt_format` AS `encrypt_format`,
`volumes`.`delete_protection` AS `delete_protection`,
`account`.`id` AS `account_id`,
`account`.`uuid` AS `account_uuid`,
`account`.`account_name` AS `account_name`,

View File

@ -935,6 +935,11 @@ public class VolumeObject implements VolumeInfo {
volumeVO.setEncryptFormat(encryptFormat);
}
@Override
public boolean isDeleteProtection() {
return volumeVO.isDeleteProtection();
}
@Override
public boolean isFollowRedirects() {
return followRedirects;

View File

@ -426,6 +426,12 @@ public class UserVmJoinDaoImpl extends GenericDaoBaseWithTagInformation<UserVmJo
userVmResponse.setDynamicallyScalable(userVm.isDynamicallyScalable());
}
if (userVm.getDeleteProtection() == null) {
userVmResponse.setDeleteProtection(false);
} else {
userVmResponse.setDeleteProtection(userVm.getDeleteProtection());
}
if (userVm.getAutoScaleVmGroupName() != null) {
userVmResponse.setAutoScaleVmGroupName(userVm.getAutoScaleVmGroupName());
}

View File

@ -138,6 +138,12 @@ public class VolumeJoinDaoImpl extends GenericDaoBaseWithTagInformation<VolumeJo
volResponse.setMinIops(volume.getMinIops());
volResponse.setMaxIops(volume.getMaxIops());
if (volume.getDeleteProtection() == null) {
volResponse.setDeleteProtection(false);
} else {
volResponse.setDeleteProtection(volume.getDeleteProtection());
}
volResponse.setCreated(volume.getCreated());
if (volume.getState() != null) {
volResponse.setState(volume.getState().toString());

View File

@ -436,6 +436,9 @@ public class UserVmJoinVO extends BaseViewWithTagInformationVO implements Contro
@Column(name = "dynamically_scalable")
private boolean isDynamicallyScalable;
@Column(name = "delete_protection")
protected Boolean deleteProtection;
public UserVmJoinVO() {
// Empty constructor
@ -946,6 +949,9 @@ public class UserVmJoinVO extends BaseViewWithTagInformationVO implements Contro
return isDynamicallyScalable;
}
public Boolean getDeleteProtection() {
return deleteProtection;
}
@Override
public Class<?> getEntityType() {

View File

@ -280,6 +280,9 @@ public class VolumeJoinVO extends BaseViewWithTagInformationVO implements Contro
@Column(name = "encrypt_format")
private String encryptionFormat = null;
@Column(name = "delete_protection")
protected Boolean deleteProtection;
public VolumeJoinVO() {
}
@ -619,6 +622,10 @@ public class VolumeJoinVO extends BaseViewWithTagInformationVO implements Contro
return encryptionFormat;
}
public Boolean getDeleteProtection() {
return deleteProtection;
}
@Override
public Class<?> getEntityType() {
return Volume.class;

View File

@ -1699,6 +1699,12 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic
}
public void validateDestroyVolume(Volume volume, Account caller, boolean expunge, boolean forceExpunge) {
if (volume.isDeleteProtection()) {
throw new InvalidParameterValueException(String.format(
"Volume [id = %s, name = %s] has delete protection enabled and cannot be deleted.",
volume.getUuid(), volume.getName()));
}
if (expunge) {
// When trying to expunge, permission is denied when the caller is not an admin and the AllowUserExpungeRecoverVolume is false for the caller.
final Long userId = caller.getAccountId();
@ -2757,13 +2763,15 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic
@Override
@ActionEvent(eventType = EventTypes.EVENT_VOLUME_UPDATE, eventDescription = "updating volume", async = true)
public Volume updateVolume(long volumeId, String path, String state, Long storageId, Boolean displayVolume,
public Volume updateVolume(long volumeId, String path, String state, Long storageId,
Boolean displayVolume, Boolean deleteProtection,
String customId, long entityOwnerId, String chainInfo, String name) {
Account caller = CallContext.current().getCallingAccount();
if (!_accountMgr.isRootAdmin(caller.getId())) {
if (path != null || state != null || storageId != null || displayVolume != null || customId != null || chainInfo != null) {
throw new InvalidParameterValueException("The domain admin and normal user are not allowed to update volume except volume name");
throw new InvalidParameterValueException("The domain admin and normal user are " +
"not allowed to update volume except volume name & delete protection");
}
}
@ -2815,6 +2823,10 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic
volume.setName(name);
}
if (deleteProtection != null) {
volume.setDeleteProtection(deleteProtection);
}
updateDisplay(volume, displayVolume);
_volsDao.update(volumeId, volume);

View File

@ -141,8 +141,14 @@ public interface UserVmManager extends UserVmService {
boolean setupVmForPvlan(boolean add, Long hostId, NicProfile nic);
UserVm updateVirtualMachine(long id, String displayName, String group, Boolean ha, Boolean isDisplayVmEnabled, Long osTypeId, String userData,
Long userDataId, String userDataDetails, Boolean isDynamicallyScalable, HTTPMethod httpMethod, String customId, String hostName, String instanceName, List<Long> securityGroupIdList, Map<String, Map<Integer, String>> extraDhcpOptionsMap) throws ResourceUnavailableException, InsufficientCapacityException;
UserVm updateVirtualMachine(long id, String displayName, String group, Boolean ha,
Boolean isDisplayVmEnabled, Boolean deleteProtection,
Long osTypeId, String userData, Long userDataId,
String userDataDetails, Boolean isDynamicallyScalable,
HTTPMethod httpMethod, String customId, String hostName,
String instanceName, List<Long> securityGroupIdList,
Map<String, Map<Integer, String>> extraDhcpOptionsMap
) throws ResourceUnavailableException, InsufficientCapacityException;
//the validateCustomParameters, save and remove CustomOfferingDetils functions can be removed from the interface once we can
//find a common place for all the scaling and upgrading code of both user and systemvms.

View File

@ -2921,8 +2921,11 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
}
}
}
return updateVirtualMachine(id, displayName, group, ha, isDisplayVm, osTypeId, userData, userDataId, userDataDetails, isDynamicallyScalable,
cmd.getHttpMethod(), cmd.getCustomId(), hostName, cmd.getInstanceName(), securityGroupIdList, cmd.getDhcpOptionsMap());
return updateVirtualMachine(id, displayName, group, ha, isDisplayVm,
cmd.getDeleteProtection(), osTypeId, userData,
userDataId, userDataDetails, isDynamicallyScalable, cmd.getHttpMethod(),
cmd.getCustomId(), hostName, cmd.getInstanceName(), securityGroupIdList,
cmd.getDhcpOptionsMap());
}
private boolean isExtraConfig(String detailName) {
@ -3023,9 +3026,14 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
}
@Override
public UserVm updateVirtualMachine(long id, String displayName, String group, Boolean ha, Boolean isDisplayVmEnabled, Long osTypeId, String userData,
Long userDataId, String userDataDetails, Boolean isDynamicallyScalable, HTTPMethod httpMethod, String customId, String hostName, String instanceName, List<Long> securityGroupIdList, Map<String, Map<Integer, String>> extraDhcpOptionsMap)
throws ResourceUnavailableException, InsufficientCapacityException {
public UserVm updateVirtualMachine(long id, String displayName, String group, Boolean ha,
Boolean isDisplayVmEnabled, Boolean deleteProtection,
Long osTypeId, String userData, Long userDataId,
String userDataDetails, Boolean isDynamicallyScalable,
HTTPMethod httpMethod, String customId, String hostName,
String instanceName, List<Long> securityGroupIdList,
Map<String, Map<Integer, String>> extraDhcpOptionsMap
) throws ResourceUnavailableException, InsufficientCapacityException {
UserVmVO vm = _vmDao.findById(id);
if (vm == null) {
throw new CloudRuntimeException("Unable to find virtual machine with id " + id);
@ -3060,6 +3068,10 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
isDisplayVmEnabled = vm.isDisplayVm();
}
if (deleteProtection == null) {
deleteProtection = vm.isDeleteProtection();
}
boolean updateUserdata = false;
if (userData != null) {
// check and replace newlines
@ -3174,7 +3186,9 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
.getUuid(), nic.getId(), extraDhcpOptionsMap);
}
_vmDao.updateVM(id, displayName, ha, osTypeId, userData, userDataId, userDataDetails, isDisplayVmEnabled, isDynamicallyScalable, customId, hostName, instanceName);
_vmDao.updateVM(id, displayName, ha, osTypeId, userData, userDataId,
userDataDetails, isDisplayVmEnabled, isDynamicallyScalable,
deleteProtection, customId, hostName, instanceName);
if (updateUserdata) {
updateUserData(vm);
@ -3411,6 +3425,12 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
return vm;
}
if (vm.isDeleteProtection()) {
throw new InvalidParameterValueException(String.format(
"Instance [id = %s, name = %s] has delete protection enabled and cannot be deleted.",
vm.getUuid(), vm.getName()));
}
// check if vm belongs to AutoScale vm group in Disabled state
autoScaleManager.checkIfVmActionAllowed(vmId);
@ -8586,6 +8606,11 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
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());
}
if (volume.isDeleteProtection()) {
throw new InvalidParameterValueException(String.format(
"Volume [id = %s, name = %s] has delete protection enabled and cannot be deleted",
volume.getUuid(), volume.getName()));
}
}
}

View File

@ -487,7 +487,7 @@ public class UserVmManagerImplTest {
Mockito.verify(userVmManagerImpl).getSecurityGroupIdList(updateVmCommand);
Mockito.verify(userVmManagerImpl).updateVirtualMachine(nullable(Long.class), nullable(String.class), nullable(String.class), nullable(Boolean.class),
nullable(Boolean.class), nullable(Long.class),
nullable(Boolean.class), nullable(Boolean.class), nullable(Long.class),
nullable(String.class), nullable(Long.class), nullable(String.class), nullable(Boolean.class), nullable(HTTPMethod.class), nullable(String.class), nullable(String.class), nullable(String.class), nullable(List.class),
nullable(Map.class));
@ -498,7 +498,7 @@ public class UserVmManagerImplTest {
Mockito.doNothing().when(userVmManagerImpl).validateInputsAndPermissionForUpdateVirtualMachineCommand(updateVmCommand);
Mockito.doReturn(new ArrayList<Long>()).when(userVmManagerImpl).getSecurityGroupIdList(updateVmCommand);
Mockito.lenient().doReturn(Mockito.mock(UserVm.class)).when(userVmManagerImpl).updateVirtualMachine(Mockito.anyLong(), Mockito.anyString(), Mockito.anyString(), Mockito.anyBoolean(),
Mockito.anyBoolean(), Mockito.anyLong(),
Mockito.anyBoolean(), Mockito.anyBoolean(), Mockito.anyLong(),
Mockito.anyString(), Mockito.anyLong(), Mockito.anyString(), Mockito.anyBoolean(), Mockito.any(HTTPMethod.class), Mockito.anyString(), Mockito.anyString(), Mockito.anyString(), Mockito.anyList(),
Mockito.anyMap());
}

View File

@ -1035,6 +1035,34 @@ class TestVMLifeCycle(cloudstackTestCase):
return
@attr(tags=["devcloud", "advanced", "advancedns", "smoke", "basic", "sg"], required_hardware="false")
def test_14_destroy_vm_delete_protection(self):
"""Test destroy Virtual Machine with delete protection
"""
# Validate the following
# 1. Should not be able to delete the VM when delete protection is enabled
# 2. Should be able to delete the VM after disabling delete protection
vm = VirtualMachine.create(
self.apiclient,
self.services["small"],
serviceofferingid=self.small_offering.id,
mode=self.services["mode"],
startvm=False
)
vm.update(self.apiclient, deleteprotection=True)
try:
vm.delete(self.apiclient)
self.fail("VM shouldn't get deleted with delete protection enabled")
except Exception as e:
self.debug("Expected exception: %s" % e)
vm.update(self.apiclient, deleteprotection=False)
vm.delete(self.apiclient)
return
class TestSecuredVmMigration(cloudstackTestCase):

View File

@ -1038,6 +1038,33 @@ class TestVolumes(cloudstackTestCase):
)
return
@attr(tags=["advanced", "advancedns", "smoke", "basic"], required_hardware="false")
def test_14_delete_volume_delete_protection(self):
"""Delete a Volume with delete protection
# Validate the following
# 1. delete volume will fail when delete protection is enabled
# 2. delete volume is successful when delete protection is disabled
"""
volume = Volume.create(
self.apiclient,
self.services,
zoneid=self.zone.id,
account=self.account.name,
domainid=self.account.domainid,
diskofferingid=self.disk_offering.id
)
volume.update(self.apiclient, deleteprotection=True)
try:
volume.delete(self.apiclient)
self.fail("Volume delete should have failed with delete protection enabled")
except Exception as e:
self.debug("Volume delete failed as expected with error: %s" % e)
volume.update(self.apiclient, deleteprotection=False)
volume.destroy(self.apiclient, expunge=True)
class TestVolumeEncryption(cloudstackTestCase):

View File

@ -1160,6 +1160,14 @@ class Volume:
return Volume(apiclient.createVolume(cmd).__dict__)
def update(self, apiclient, **kwargs):
"""Updates the volume"""
cmd = updateVolume.updateVolumeCmd()
cmd.id = self.id
[setattr(cmd, k, v) for k, v in list(kwargs.items())]
return (apiclient.updateVolume(cmd))
@classmethod
def create_custom_disk(cls, apiclient, services, account=None,
domainid=None, diskofferingid=None, projectid=None):

View File

@ -740,6 +740,7 @@
"label.deleting.iso": "Deleting ISO",
"label.deleting.snapshot": "Deleting Snapshot",
"label.deleting.template": "Deleting Template",
"label.deleteprotection": "Delete protection",
"label.demote.project.owner": "Demote Account to regular role",
"label.demote.project.owner.user": "Demote User to regular role",
"label.deny": "Deny",

View File

@ -81,7 +81,8 @@ export default {
details: () => {
var fields = ['name', 'displayname', 'id', 'state', 'ipaddress', 'ip6address', 'templatename', 'ostypename',
'serviceofferingname', 'isdynamicallyscalable', 'haenable', 'hypervisor', 'boottype', 'bootmode', 'account',
'domain', 'zonename', 'userdataid', 'userdataname', 'userdataparams', 'userdatadetails', 'userdatapolicy', 'hostcontrolstate']
'domain', 'zonename', 'userdataid', 'userdataname', 'userdataparams', 'userdatadetails', 'userdatapolicy',
'hostcontrolstate', 'deleteprotection']
const listZoneHaveSGEnabled = store.getters.zones.filter(zone => zone.securitygroupsenabled === true)
if (!listZoneHaveSGEnabled || listZoneHaveSGEnabled.length === 0) {
return fields

View File

@ -62,7 +62,7 @@ export default {
return fields
},
details: ['name', 'id', 'type', 'storagetype', 'diskofferingdisplaytext', 'deviceid', 'sizegb', 'physicalsize', 'provisioningtype', 'utilization', 'diskkbsread', 'diskkbswrite', 'diskioread', 'diskiowrite', 'diskiopstotal', 'miniops', 'maxiops', 'path'],
details: ['name', 'id', 'type', 'storagetype', 'diskofferingdisplaytext', 'deviceid', 'sizegb', 'physicalsize', 'provisioningtype', 'utilization', 'diskkbsread', 'diskkbswrite', 'diskioread', 'diskiowrite', 'diskiopstotal', 'miniops', 'maxiops', 'path', 'deleteprotection'],
related: [{
name: 'snapshot',
title: 'label.snapshots',
@ -148,7 +148,7 @@ export default {
icon: 'edit-outlined',
label: 'label.edit',
dataView: true,
args: ['name'],
args: ['name', 'deleteprotection'],
mapping: {
account: {
value: (record) => { return record.account }

View File

@ -111,6 +111,13 @@
</a-select>
</a-form-item>
<a-form-item name="deleteprotection" ref="deleteprotection">
<template #label>
<tooltip-label :title="$t('label.deleteprotection')" :tooltip="apiParams.deleteprotection.description"/>
</template>
<a-switch v-model:checked="form.deleteprotection" />
</a-form-item>
<div :span="24" class="action-button">
<a-button :loading="loading" @click="onCloseAction">{{ $t('label.cancel') }}</a-button>
<a-button :loading="loading" ref="submit" type="primary" @click="handleSubmit">{{ $t('label.ok') }}</a-button>
@ -175,6 +182,7 @@ export default {
displayname: this.resource.displayname,
ostypeid: this.resource.ostypeid,
isdynamicallyscalable: this.resource.isdynamicallyscalable,
deleteprotection: this.resource.deleteprotection,
group: this.resource.group,
securitygroupids: this.resource.securitygroup.map(x => x.id),
userdata: '',
@ -314,6 +322,9 @@ export default {
if (values.isdynamicallyscalable !== undefined) {
params.isdynamicallyscalable = values.isdynamicallyscalable
}
if (values.deleteprotection !== undefined) {
params.deleteprotection = values.deleteprotection
}
if (values.haenable !== undefined) {
params.haenable = values.haenable
}