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); 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; 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. * 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 DATACENTER_NAME = "datacentername";
public static final String DATADISK_OFFERING_LIST = "datadiskofferinglist"; public static final String DATADISK_OFFERING_LIST = "datadiskofferinglist";
public static final String DEFAULT_VALUE = "defaultvalue"; public static final String DEFAULT_VALUE = "defaultvalue";
public static final String DELETE_PROTECTION = "deleteprotection";
public static final String DESCRIPTION = "description"; public static final String DESCRIPTION = "description";
public static final String DESTINATION = "destination"; public static final String DESTINATION = "destination";
public static final String DESTINATION_ZONE_ID = "destzoneid"; 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) @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; 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 /////////////////////// /////////////////// Accessors ///////////////////////
///////////////////////////////////////////////////// /////////////////////////////////////////////////////
@ -215,6 +223,10 @@ public class UpdateVMCmd extends BaseCustomIdCmd implements SecurityGroupAction,
return cleanupDetails == null ? false : cleanupDetails.booleanValue(); return cleanupDetails == null ? false : cleanupDetails.booleanValue();
} }
public Boolean getDeleteProtection() {
return deleteProtection;
}
public Map<String, Map<Integer, String>> getDhcpOptionsMap() { public Map<String, Map<Integer, String>> getDhcpOptionsMap() {
Map<String, Map<Integer, String>> dhcpOptionsMap = new HashMap<>(); Map<String, Map<Integer, String>> dhcpOptionsMap = new HashMap<>();
if (dhcpOptionsNetworkList != null && !dhcpOptionsNetworkList.isEmpty()) { 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") @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, description = "new name of the volume", since = "4.16")
private String name; 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 /////////////////////// /////////////////// Accessors ///////////////////////
///////////////////////////////////////////////////// /////////////////////////////////////////////////////
@ -109,6 +117,10 @@ public class UpdateVolumeCmd extends BaseAsyncCustomIdCmd implements UserCmd {
return name; return name;
} }
public Boolean getDeleteProtection() {
return deleteProtection;
}
///////////////////////////////////////////////////// /////////////////////////////////////////////////////
/////////////// API Implementation/////////////////// /////////////// API Implementation///////////////////
///////////////////////////////////////////////////// /////////////////////////////////////////////////////
@ -168,7 +180,7 @@ public class UpdateVolumeCmd extends BaseAsyncCustomIdCmd implements UserCmd {
public void execute() { public void execute() {
CallContext.current().setEventDetails("Volume Id: " + this._uuidMgr.getUuid(Volume.class, getId())); CallContext.current().setEventDetails("Volume Id: " + this._uuidMgr.getUuid(Volume.class, getId()));
Volume result = _volumeService.updateVolume(getId(), getPath(), getState(), getStorageId(), getDisplayVolume(), Volume result = _volumeService.updateVolume(getId(), getPath(), getState(), getStorageId(), getDisplayVolume(),
getCustomId(), getEntityOwnerId(), getChainInfo(), getName()); getDeleteProtection(), getCustomId(), getEntityOwnerId(), getChainInfo(), getName());
if (result != null) { if (result != null) {
VolumeResponse response = _responseGenerator.createVolumeResponse(getResponseView(), result); VolumeResponse response = _responseGenerator.createVolumeResponse(getResponseView(), result);
response.setResponseName(getCommandName()); 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.") @Param(description = "true if vm contains XS/VMWare tools inorder to support dynamic scaling of VM cpu/memory.")
private Boolean isDynamicallyScalable; 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) @SerializedName(ApiConstants.SERVICE_STATE)
@Param(description = "State of the Service from LB rule") @Param(description = "State of the Service from LB rule")
private String serviceState; private String serviceState;
@ -995,6 +999,14 @@ public class UserVmResponse extends BaseResponseWithTagInformation implements Co
isDynamicallyScalable = dynamicallyScalable; isDynamicallyScalable = dynamicallyScalable;
} }
public boolean isDeleteProtection() {
return deleteProtection;
}
public void setDeleteProtection(boolean deleteProtection) {
this.deleteProtection = deleteProtection;
}
public String getOsTypeId() { public String getOsTypeId() {
return osTypeId; 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") @Param(description = "true if storage snapshot is supported for the volume, false otherwise", since = "4.16")
private boolean supportsStorageSnapshot; 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) @SerializedName(ApiConstants.PHYSICAL_SIZE)
@Param(description = "the bytes actually consumed on disk") @Param(description = "the bytes actually consumed on disk")
private Long physicalsize; private Long physicalsize;
@ -584,6 +588,14 @@ public class VolumeResponse extends BaseResponseWithTagInformation implements Co
return this.supportsStorageSnapshot; return this.supportsStorageSnapshot;
} }
public boolean isDeleteProtection() {
return deleteProtection;
}
public void setDeleteProtection(boolean deleteProtection) {
this.deleteProtection = deleteProtection;
}
public String getIsoId() { public String getIsoId() {
return isoId; return isoId;
} }

View File

@ -182,6 +182,9 @@ public class VolumeVO implements Volume {
@Column(name = "encrypt_format") @Column(name = "encrypt_format")
private String encryptFormat; private String encryptFormat;
@Column(name = "delete_protection")
private boolean deleteProtection;
// Real Constructor // Real Constructor
public VolumeVO(Type type, String name, long dcId, long domainId, 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 String getEncryptFormat() { return encryptFormat; }
public void setEncryptFormat(String encryptFormat) { this.encryptFormat = 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") @Column(name = "dynamically_scalable")
protected boolean dynamicallyScalable; protected boolean dynamicallyScalable;
/* @Column(name = "delete_protection")
@Column(name="tags") protected boolean deleteProtection;
protected String tags;
*/
@Transient @Transient
Map<String, String> details; Map<String, String> details;
@ -542,6 +540,14 @@ public class VMInstanceVO implements VirtualMachine, FiniteStateObject<State, Vi
return dynamicallyScalable; return dynamicallyScalable;
} }
public boolean isDeleteProtection() {
return deleteProtection;
}
public void setDeleteProtection(boolean deleteProtection) {
this.deleteProtection = deleteProtection;
}
@Override @Override
public Class<?> getEntityType() { public Class<?> getEntityType() {
return VirtualMachine.class; return VirtualMachine.class;

View File

@ -53,7 +53,11 @@ public interface UserVmDao extends GenericDao<UserVmVO, Long> {
* @param hostName TODO * @param hostName TODO
* @param instanceName * @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); List<UserVmVO> findDestroyedVms(Date date);

View File

@ -274,8 +274,11 @@ public class UserVmDaoImpl extends GenericDaoBase<UserVmVO, Long> implements Use
} }
@Override @Override
public void updateVM(long id, String displayName, boolean enable, Long osTypeId, String userData, Long userDataId, String userDataDetails, boolean displayVm, public void updateVM(long id, String displayName, boolean enable, Long osTypeId,
boolean isDynamicallyScalable, String customId, String hostName, String instanceName) { String userData, Long userDataId, String userDataDetails,
boolean displayVm, boolean isDynamicallyScalable,
boolean deleteProtection, String customId, String hostName,
String instanceName) {
UserVmVO vo = createForUpdate(); UserVmVO vo = createForUpdate();
vo.setDisplayName(displayName); vo.setDisplayName(displayName);
vo.setHaEnabled(enable); vo.setHaEnabled(enable);
@ -285,6 +288,7 @@ public class UserVmDaoImpl extends GenericDaoBase<UserVmVO, Long> implements Use
vo.setUserDataDetails(userDataDetails); vo.setUserDataDetails(userDataDetails);
vo.setDisplayVm(displayVm); vo.setDisplayVm(displayVm);
vo.setDynamicallyScalable(isDynamicallyScalable); vo.setDynamicallyScalable(isDynamicallyScalable);
vo.setDeleteProtection(deleteProtection);
if (hostName != null) { if (hostName != null) {
vo.setHostName(hostName); 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`.`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`.`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'; 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`.`instance_name` AS `instance_name`,
`vm_instance`.`guest_os_id` AS `guest_os_id`, `vm_instance`.`guest_os_id` AS `guest_os_id`,
`vm_instance`.`display_vm` AS `display_vm`, `vm_instance`.`display_vm` AS `display_vm`,
`vm_instance`.`delete_protection` AS `delete_protection`,
`guest_os`.`uuid` AS `guest_os_uuid`, `guest_os`.`uuid` AS `guest_os_uuid`,
`vm_instance`.`pod_id` AS `pod_id`, `vm_instance`.`pod_id` AS `pod_id`,
`host_pod_ref`.`uuid` AS `pod_uuid`, `host_pod_ref`.`uuid` AS `pod_uuid`,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -280,6 +280,9 @@ public class VolumeJoinVO extends BaseViewWithTagInformationVO implements Contro
@Column(name = "encrypt_format") @Column(name = "encrypt_format")
private String encryptionFormat = null; private String encryptionFormat = null;
@Column(name = "delete_protection")
protected Boolean deleteProtection;
public VolumeJoinVO() { public VolumeJoinVO() {
} }
@ -619,6 +622,10 @@ public class VolumeJoinVO extends BaseViewWithTagInformationVO implements Contro
return encryptionFormat; return encryptionFormat;
} }
public Boolean getDeleteProtection() {
return deleteProtection;
}
@Override @Override
public Class<?> getEntityType() { public Class<?> getEntityType() {
return Volume.class; 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) { 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) { if (expunge) {
// When trying to expunge, permission is denied when the caller is not an admin and the AllowUserExpungeRecoverVolume is false for the caller. // 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(); final Long userId = caller.getAccountId();
@ -2757,13 +2763,15 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic
@Override @Override
@ActionEvent(eventType = EventTypes.EVENT_VOLUME_UPDATE, eventDescription = "updating volume", async = true) @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) { String customId, long entityOwnerId, String chainInfo, String name) {
Account caller = CallContext.current().getCallingAccount(); Account caller = CallContext.current().getCallingAccount();
if (!_accountMgr.isRootAdmin(caller.getId())) { if (!_accountMgr.isRootAdmin(caller.getId())) {
if (path != null || state != null || storageId != null || displayVolume != null || customId != null || chainInfo != null) { 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); volume.setName(name);
} }
if (deleteProtection != null) {
volume.setDeleteProtection(deleteProtection);
}
updateDisplay(volume, displayVolume); updateDisplay(volume, displayVolume);
_volsDao.update(volumeId, volume); _volsDao.update(volumeId, volume);

View File

@ -141,8 +141,14 @@ public interface UserVmManager extends UserVmService {
boolean setupVmForPvlan(boolean add, Long hostId, NicProfile nic); boolean setupVmForPvlan(boolean add, Long hostId, NicProfile nic);
UserVm updateVirtualMachine(long id, String displayName, String group, Boolean ha, Boolean isDisplayVmEnabled, Long osTypeId, String userData, UserVm updateVirtualMachine(long id, String displayName, String group, Boolean ha,
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; 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 //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. //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, return updateVirtualMachine(id, displayName, group, ha, isDisplayVm,
cmd.getHttpMethod(), cmd.getCustomId(), hostName, cmd.getInstanceName(), securityGroupIdList, cmd.getDhcpOptionsMap()); cmd.getDeleteProtection(), osTypeId, userData,
userDataId, userDataDetails, isDynamicallyScalable, cmd.getHttpMethod(),
cmd.getCustomId(), hostName, cmd.getInstanceName(), securityGroupIdList,
cmd.getDhcpOptionsMap());
} }
private boolean isExtraConfig(String detailName) { private boolean isExtraConfig(String detailName) {
@ -3023,9 +3026,14 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
} }
@Override @Override
public UserVm updateVirtualMachine(long id, String displayName, String group, Boolean ha, Boolean isDisplayVmEnabled, Long osTypeId, String userData, public UserVm updateVirtualMachine(long id, String displayName, String group, Boolean ha,
Long userDataId, String userDataDetails, Boolean isDynamicallyScalable, HTTPMethod httpMethod, String customId, String hostName, String instanceName, List<Long> securityGroupIdList, Map<String, Map<Integer, String>> extraDhcpOptionsMap) Boolean isDisplayVmEnabled, Boolean deleteProtection,
throws ResourceUnavailableException, InsufficientCapacityException { 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); UserVmVO vm = _vmDao.findById(id);
if (vm == null) { if (vm == null) {
throw new CloudRuntimeException("Unable to find virtual machine with id " + id); 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(); isDisplayVmEnabled = vm.isDisplayVm();
} }
if (deleteProtection == null) {
deleteProtection = vm.isDeleteProtection();
}
boolean updateUserdata = false; boolean updateUserdata = false;
if (userData != null) { if (userData != null) {
// check and replace newlines // check and replace newlines
@ -3174,7 +3186,9 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
.getUuid(), nic.getId(), extraDhcpOptionsMap); .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) { if (updateUserdata) {
updateUserData(vm); updateUserData(vm);
@ -3411,6 +3425,12 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
return vm; 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 // check if vm belongs to AutoScale vm group in Disabled state
autoScaleManager.checkIfVmActionAllowed(vmId); 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)) { 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()); 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).getSecurityGroupIdList(updateVmCommand);
Mockito.verify(userVmManagerImpl).updateVirtualMachine(nullable(Long.class), nullable(String.class), nullable(String.class), nullable(Boolean.class), 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(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)); nullable(Map.class));
@ -498,7 +498,7 @@ public class UserVmManagerImplTest {
Mockito.doNothing().when(userVmManagerImpl).validateInputsAndPermissionForUpdateVirtualMachineCommand(updateVmCommand); Mockito.doNothing().when(userVmManagerImpl).validateInputsAndPermissionForUpdateVirtualMachineCommand(updateVmCommand);
Mockito.doReturn(new ArrayList<Long>()).when(userVmManagerImpl).getSecurityGroupIdList(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.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.anyString(), Mockito.anyLong(), Mockito.anyString(), Mockito.anyBoolean(), Mockito.any(HTTPMethod.class), Mockito.anyString(), Mockito.anyString(), Mockito.anyString(), Mockito.anyList(),
Mockito.anyMap()); Mockito.anyMap());
} }

View File

@ -1035,6 +1035,34 @@ class TestVMLifeCycle(cloudstackTestCase):
return 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): class TestSecuredVmMigration(cloudstackTestCase):

View File

@ -1038,6 +1038,33 @@ class TestVolumes(cloudstackTestCase):
) )
return 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): class TestVolumeEncryption(cloudstackTestCase):

View File

@ -1160,6 +1160,14 @@ class Volume:
return Volume(apiclient.createVolume(cmd).__dict__) 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 @classmethod
def create_custom_disk(cls, apiclient, services, account=None, def create_custom_disk(cls, apiclient, services, account=None,
domainid=None, diskofferingid=None, projectid=None): domainid=None, diskofferingid=None, projectid=None):

View File

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

View File

@ -81,7 +81,8 @@ export default {
details: () => { details: () => {
var fields = ['name', 'displayname', 'id', 'state', 'ipaddress', 'ip6address', 'templatename', 'ostypename', var fields = ['name', 'displayname', 'id', 'state', 'ipaddress', 'ip6address', 'templatename', 'ostypename',
'serviceofferingname', 'isdynamicallyscalable', 'haenable', 'hypervisor', 'boottype', 'bootmode', 'account', '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) const listZoneHaveSGEnabled = store.getters.zones.filter(zone => zone.securitygroupsenabled === true)
if (!listZoneHaveSGEnabled || listZoneHaveSGEnabled.length === 0) { if (!listZoneHaveSGEnabled || listZoneHaveSGEnabled.length === 0) {
return fields return fields

View File

@ -62,7 +62,7 @@ export default {
return fields 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: [{ related: [{
name: 'snapshot', name: 'snapshot',
title: 'label.snapshots', title: 'label.snapshots',
@ -148,7 +148,7 @@ export default {
icon: 'edit-outlined', icon: 'edit-outlined',
label: 'label.edit', label: 'label.edit',
dataView: true, dataView: true,
args: ['name'], args: ['name', 'deleteprotection'],
mapping: { mapping: {
account: { account: {
value: (record) => { return record.account } value: (record) => { return record.account }

View File

@ -111,6 +111,13 @@
</a-select> </a-select>
</a-form-item> </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"> <div :span="24" class="action-button">
<a-button :loading="loading" @click="onCloseAction">{{ $t('label.cancel') }}</a-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> <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, displayname: this.resource.displayname,
ostypeid: this.resource.ostypeid, ostypeid: this.resource.ostypeid,
isdynamicallyscalable: this.resource.isdynamicallyscalable, isdynamicallyscalable: this.resource.isdynamicallyscalable,
deleteprotection: this.resource.deleteprotection,
group: this.resource.group, group: this.resource.group,
securitygroupids: this.resource.securitygroup.map(x => x.id), securitygroupids: this.resource.securitygroup.map(x => x.id),
userdata: '', userdata: '',
@ -314,6 +322,9 @@ export default {
if (values.isdynamicallyscalable !== undefined) { if (values.isdynamicallyscalable !== undefined) {
params.isdynamicallyscalable = values.isdynamicallyscalable params.isdynamicallyscalable = values.isdynamicallyscalable
} }
if (values.deleteprotection !== undefined) {
params.deleteprotection = values.deleteprotection
}
if (values.haenable !== undefined) { if (values.haenable !== undefined) {
params.haenable = values.haenable params.haenable = values.haenable
} }