Merge remote-tracking branch 'apache/4.19'

This commit is contained in:
Wei Zhou 2024-04-12 16:40:07 +02:00
commit 45daa1ce59
No known key found for this signature in database
GPG Key ID: 1503DFE7C8226103
14 changed files with 542 additions and 135 deletions

View File

@ -175,6 +175,8 @@ public interface VolumeApiService {
boolean validateVolumeSizeInBytes(long size);
void validateDestroyVolume(Volume volume, Account caller, boolean expunge, boolean forceExpunge);
Volume changeDiskOfferingForVolume(ChangeOfferingForVolumeCmd cmd) throws ResourceAllocationException;
void publishVolumeCreationUsageEvent(Volume volume);

View File

@ -492,7 +492,7 @@ public interface UserVmService {
UserVm restoreVM(RestoreVMCmd cmd) throws InsufficientCapacityException, ResourceUnavailableException;
UserVm restoreVirtualMachine(Account caller, long vmId, Long newTemplateId) throws InsufficientCapacityException, ResourceUnavailableException;
UserVm restoreVirtualMachine(Account caller, long vmId, Long newTemplateId, Long rootDiskOfferingId, boolean expunge, Map<String, String> details) throws InsufficientCapacityException, ResourceUnavailableException;
UserVm upgradeVirtualMachine(ScaleVMCmd cmd) throws ResourceUnavailableException, ConcurrentOperationException, ManagementServerException,
VirtualMachineMigrationException;

View File

@ -16,9 +16,9 @@
// under the License.
package org.apache.cloudstack.api.command.user.vm;
import org.apache.cloudstack.api.ApiCommandResourceType;
import com.cloud.vm.VmDetailConstants;
import org.apache.cloudstack.acl.SecurityChecker.AccessType;
import org.apache.cloudstack.api.ApiCommandResourceType;
import org.apache.cloudstack.api.ACL;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiConstants;
@ -28,6 +28,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.command.user.UserCmd;
import org.apache.cloudstack.api.response.DiskOfferingResponse;
import org.apache.cloudstack.api.response.TemplateResponse;
import org.apache.cloudstack.api.response.UserVmResponse;
import org.apache.cloudstack.context.CallContext;
@ -41,6 +42,8 @@ import com.cloud.user.Account;
import com.cloud.uservm.UserVm;
import com.cloud.vm.VirtualMachine;
import java.util.Map;
@APICommand(name = "restoreVirtualMachine", description = "Restore a VM to original template/ISO or new template/ISO", responseObject = UserVmResponse.class, since = "3.0.0", responseView = ResponseView.Restricted, entityType = {VirtualMachine.class},
requestHasSensitiveInfo = false,
responseHasSensitiveInfo = true)
@ -58,6 +61,28 @@ public class RestoreVMCmd extends BaseAsyncCmd implements UserCmd {
description = "an optional template Id to restore vm from the new template. This can be an ISO id in case of restore vm deployed using ISO")
private Long templateId;
@Parameter(name = ApiConstants.DISK_OFFERING_ID,
type = CommandType.UUID,
entityType = DiskOfferingResponse.class,
description = "Override root volume's diskoffering.", since = "4.19.1")
private Long rootDiskOfferingId;
@Parameter(name = ApiConstants.ROOT_DISK_SIZE,
type = CommandType.LONG,
description = "Override root volume's size (in GB). Analogous to details[0].rootdisksize, which takes precedence over this parameter if both are provided",
since = "4.19.1")
private Long rootDiskSize;
@Parameter(name = ApiConstants.DETAILS, type = CommandType.MAP, since = "4.19.1",
description = "used to specify the custom parameters")
private Map details;
@Parameter(name = ApiConstants.EXPUNGE,
type = CommandType.BOOLEAN,
description = "Optional field to expunge old root volume after restore.",
since = "4.19.1")
private Boolean expungeRootDisk;
@Override
public String getEventType() {
return EventTypes.EVENT_VM_RESTORE;
@ -110,6 +135,22 @@ public class RestoreVMCmd extends BaseAsyncCmd implements UserCmd {
return getVmId();
}
public Long getRootDiskOfferingId() {
return rootDiskOfferingId;
}
public Map<String, String> getDetails() {
Map<String, String> customparameterMap = convertDetailsToMap(details);
if (rootDiskSize != null && !customparameterMap.containsKey(VmDetailConstants.ROOT_DISK_SIZE)) {
customparameterMap.put(VmDetailConstants.ROOT_DISK_SIZE, rootDiskSize.toString());
}
return customparameterMap;
}
public Boolean getExpungeRootDisk() {
return expungeRootDisk != null && expungeRootDisk;
}
@Override
public Long getApiResourceId() {
return getId();

View File

@ -254,7 +254,7 @@ public interface VirtualMachineManager extends Manager {
*/
boolean unmanage(String vmUuid);
UserVm restoreVirtualMachine(long vmId, Long newTemplateId) throws ResourceUnavailableException, InsufficientCapacityException;
UserVm restoreVirtualMachine(long vmId, Long newTemplateId, Long rootDiskOfferingId, boolean expunge, Map<String, String> details) throws ResourceUnavailableException, InsufficientCapacityException;
boolean checkIfVmHasClusterWideVolumes(Long vmId);

View File

@ -5692,20 +5692,20 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac
}
@Override
public UserVm restoreVirtualMachine(final long vmId, final Long newTemplateId) throws ResourceUnavailableException, InsufficientCapacityException {
public UserVm restoreVirtualMachine(final long vmId, final Long newTemplateId, final Long rootDiskOfferingId, final boolean expunge, final Map<String, String> details) throws ResourceUnavailableException, InsufficientCapacityException {
final AsyncJobExecutionContext jobContext = AsyncJobExecutionContext.getCurrentExecutionContext();
if (jobContext.isJobDispatchedBy(VmWorkConstants.VM_WORK_JOB_DISPATCHER)) {
VmWorkJobVO placeHolder = null;
placeHolder = createPlaceHolderWork(vmId);
try {
return orchestrateRestoreVirtualMachine(vmId, newTemplateId);
return orchestrateRestoreVirtualMachine(vmId, newTemplateId, rootDiskOfferingId, expunge, details);
} finally {
if (placeHolder != null) {
_workJobDao.expunge(placeHolder.getId());
}
}
} else {
final Outcome<VirtualMachine> outcome = restoreVirtualMachineThroughJobQueue(vmId, newTemplateId);
final Outcome<VirtualMachine> outcome = restoreVirtualMachineThroughJobQueue(vmId, newTemplateId, rootDiskOfferingId, expunge, details);
retrieveVmFromJobOutcome(outcome, String.valueOf(vmId), "restoreVirtualMachine");
@ -5722,14 +5722,14 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac
}
}
private UserVm orchestrateRestoreVirtualMachine(final long vmId, final Long newTemplateId) throws ResourceUnavailableException, InsufficientCapacityException {
logger.debug("Restoring vm " + vmId + " with new templateId " + newTemplateId);
private UserVm orchestrateRestoreVirtualMachine(final long vmId, final Long newTemplateId, final Long rootDiskOfferingId, final boolean expunge, final Map<String, String> details) throws ResourceUnavailableException, InsufficientCapacityException {
logger.debug("Restoring vm " + vmId + " with templateId : " + newTemplateId + " diskOfferingId : " + rootDiskOfferingId + " details : " + details);
final CallContext context = CallContext.current();
final Account account = context.getCallingAccount();
return _userVmService.restoreVirtualMachine(account, vmId, newTemplateId);
return _userVmService.restoreVirtualMachine(account, vmId, newTemplateId, rootDiskOfferingId, expunge, details);
}
public Outcome<VirtualMachine> restoreVirtualMachineThroughJobQueue(final long vmId, final Long newTemplateId) {
public Outcome<VirtualMachine> restoreVirtualMachineThroughJobQueue(final long vmId, final Long newTemplateId, final Long rootDiskOfferingId, final boolean expunge, Map<String, String> details) {
String commandName = VmWorkRestore.class.getName();
Pair<VmWorkJobVO, Long> pendingWorkJob = retrievePendingWorkJob(vmId, commandName);
@ -5739,7 +5739,7 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac
Pair<VmWorkJobVO, VmWork> newVmWorkJobAndInfo = createWorkJobAndWorkInfo(commandName, vmId);
workJob = newVmWorkJobAndInfo.first();
VmWorkRestore workInfo = new VmWorkRestore(newVmWorkJobAndInfo.second(), newTemplateId);
VmWorkRestore workInfo = new VmWorkRestore(newVmWorkJobAndInfo.second(), newTemplateId, rootDiskOfferingId, expunge, details);
setCmdInfoAndSubmitAsyncJob(workJob, workInfo, vmId);
}
@ -5751,7 +5751,7 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac
@ReflectionUse
private Pair<JobInfo.Status, String> orchestrateRestoreVirtualMachine(final VmWorkRestore work) throws Exception {
VMInstanceVO vm = findVmById(work.getVmId());
UserVm uservm = orchestrateRestoreVirtualMachine(vm.getId(), work.getTemplateId());
UserVm uservm = orchestrateRestoreVirtualMachine(vm.getId(), work.getTemplateId(), work.getRootDiskOfferingId(), work.getExpunge(), work.getDetails());
HashMap<Long, String> passwordMap = new HashMap<>();
passwordMap.put(uservm.getId(), uservm.getPassword());
return new Pair<>(JobInfo.Status.SUCCEEDED, _jobMgr.marshallResultObject(passwordMap));

View File

@ -16,23 +16,38 @@
// under the License.
package com.cloud.vm;
import java.util.Map;
public class VmWorkRestore extends VmWork {
private static final long serialVersionUID = 195901782359759635L;
private Long templateId;
private Long rootDiskOfferingId;
private Map<String,String> details;
public VmWorkRestore(long userId, long accountId, long vmId, String handlerName, Long templateId) {
super(userId, accountId, vmId, handlerName);
private boolean expunge;
this.templateId = templateId;
}
public VmWorkRestore(VmWork vmWork, Long templateId) {
public VmWorkRestore(VmWork vmWork, Long templateId, Long rootDiskOfferingId, boolean expunge, Map<String,String> details) {
super(vmWork);
this.templateId = templateId;
this.rootDiskOfferingId = rootDiskOfferingId;
this.expunge = expunge;
this.details = details;
}
public Long getTemplateId() {
return templateId;
}
public Long getRootDiskOfferingId() {
return rootDiskOfferingId;
}
public boolean getExpunge() {
return expunge;
}
public Map<String, String> getDetails() {
return details;
}
}

View File

@ -61,6 +61,9 @@ import com.cloud.vm.dao.UserVmDao;
import com.cloud.vm.dao.UserVmDetailsDao;
import com.cloud.vm.dao.VMInstanceDao;
import static org.apache.cloudstack.api.ApiConstants.MAX_IOPS;
import static org.apache.cloudstack.api.ApiConstants.MIN_IOPS;
@Component
public class CloudOrchestrator implements OrchestrationService {
@ -196,8 +199,8 @@ public class CloudOrchestrator implements OrchestrationService {
Map<String, String> userVmDetails = _userVmDetailsDao.listDetailsKeyPairs(vm.getId());
if (userVmDetails != null) {
String minIops = userVmDetails.get("minIops");
String maxIops = userVmDetails.get("maxIops");
String minIops = userVmDetails.get(MIN_IOPS);
String maxIops = userVmDetails.get(MAX_IOPS);
rootDiskOfferingInfo.setMinIops(minIops != null && minIops.trim().length() > 0 ? Long.parseLong(minIops) : null);
rootDiskOfferingInfo.setMaxIops(maxIops != null && maxIops.trim().length() > 0 ? Long.parseLong(maxIops) : null);

View File

@ -879,32 +879,6 @@ public class VolumeOrchestrator extends ManagerBase implements VolumeOrchestrati
return diskProfile;
}
@Override
public void saveVolumeDetails(Long diskOfferingId, Long volumeId) {
List<VolumeDetailVO> volumeDetailsVO = new ArrayList<>();
DiskOfferingDetailVO bandwidthLimitDetail = _diskOfferingDetailDao.findDetail(diskOfferingId, Volume.BANDWIDTH_LIMIT_IN_MBPS);
if (bandwidthLimitDetail != null) {
volumeDetailsVO.add(new VolumeDetailVO(volumeId, Volume.BANDWIDTH_LIMIT_IN_MBPS, bandwidthLimitDetail.getValue(), false));
} else {
VolumeDetailVO bandwidthLimit = _volDetailDao.findDetail(volumeId, Volume.BANDWIDTH_LIMIT_IN_MBPS);
if (bandwidthLimit != null) {
_volDetailDao.remove(bandwidthLimit.getId());
}
}
DiskOfferingDetailVO iopsLimitDetail = _diskOfferingDetailDao.findDetail(diskOfferingId, Volume.IOPS_LIMIT);
if (iopsLimitDetail != null) {
volumeDetailsVO.add(new VolumeDetailVO(volumeId, Volume.IOPS_LIMIT, iopsLimitDetail.getValue(), false));
} else {
VolumeDetailVO iopsLimit = _volDetailDao.findDetail(volumeId, Volume.IOPS_LIMIT);
if (iopsLimit != null) {
_volDetailDao.remove(iopsLimit.getId());
}
}
if (!volumeDetailsVO.isEmpty()) {
_volDetailDao.saveDetails(volumeDetailsVO);
}
}
private DiskProfile allocateTemplatedVolume(Type type, String name, DiskOffering offering, Long rootDisksize, Long minIops, Long maxIops, VirtualMachineTemplate template, VirtualMachine vm,
Account owner, long deviceId, String configurationId) {
assert (template.getFormat() != ImageFormat.ISO) : "ISO is not a template.";
@ -948,18 +922,7 @@ public class VolumeOrchestrator extends ManagerBase implements VolumeOrchestrati
vol = _volsDao.persist(vol);
List<VolumeDetailVO> volumeDetailsVO = new ArrayList<VolumeDetailVO>();
DiskOfferingDetailVO bandwidthLimitDetail = _diskOfferingDetailDao.findDetail(offering.getId(), Volume.BANDWIDTH_LIMIT_IN_MBPS);
if (bandwidthLimitDetail != null) {
volumeDetailsVO.add(new VolumeDetailVO(vol.getId(), Volume.BANDWIDTH_LIMIT_IN_MBPS, bandwidthLimitDetail.getValue(), false));
}
DiskOfferingDetailVO iopsLimitDetail = _diskOfferingDetailDao.findDetail(offering.getId(), Volume.IOPS_LIMIT);
if (iopsLimitDetail != null) {
volumeDetailsVO.add(new VolumeDetailVO(vol.getId(), Volume.IOPS_LIMIT, iopsLimitDetail.getValue(), false));
}
if (!volumeDetailsVO.isEmpty()) {
_volDetailDao.saveDetails(volumeDetailsVO);
}
saveVolumeDetails(offering.getId(), vol.getId());
if (StringUtils.isNotBlank(configurationId)) {
VolumeDetailVO deployConfigurationDetail = new VolumeDetailVO(vol.getId(), VmDetailConstants.DEPLOY_AS_IS_CONFIGURATION, configurationId, false);
@ -983,6 +946,32 @@ public class VolumeOrchestrator extends ManagerBase implements VolumeOrchestrati
return toDiskProfile(vol, offering);
}
@Override
public void saveVolumeDetails(Long diskOfferingId, Long volumeId) {
List<VolumeDetailVO> volumeDetailsVO = new ArrayList<>();
DiskOfferingDetailVO bandwidthLimitDetail = _diskOfferingDetailDao.findDetail(diskOfferingId, Volume.BANDWIDTH_LIMIT_IN_MBPS);
if (bandwidthLimitDetail != null) {
volumeDetailsVO.add(new VolumeDetailVO(volumeId, Volume.BANDWIDTH_LIMIT_IN_MBPS, bandwidthLimitDetail.getValue(), false));
} else {
VolumeDetailVO bandwidthLimit = _volDetailDao.findDetail(volumeId, Volume.BANDWIDTH_LIMIT_IN_MBPS);
if (bandwidthLimit != null) {
_volDetailDao.remove(bandwidthLimit.getId());
}
}
DiskOfferingDetailVO iopsLimitDetail = _diskOfferingDetailDao.findDetail(diskOfferingId, Volume.IOPS_LIMIT);
if (iopsLimitDetail != null) {
volumeDetailsVO.add(new VolumeDetailVO(volumeId, Volume.IOPS_LIMIT, iopsLimitDetail.getValue(), false));
} else {
VolumeDetailVO iopsLimit = _volDetailDao.findDetail(volumeId, Volume.IOPS_LIMIT);
if (iopsLimit != null) {
_volDetailDao.remove(iopsLimit.getId());
}
}
if (!volumeDetailsVO.isEmpty()) {
_volDetailDao.saveDetails(volumeDetailsVO);
}
}
@ActionEvent(eventType = EventTypes.EVENT_VOLUME_CREATE, eventDescription = "creating ROOT volume", create = true)
@Override
public List<DiskProfile> allocateTemplatedVolumes(Type type, String name, DiskOffering offering, Long rootDisksize, Long minIops, Long maxIops, VirtualMachineTemplate template, VirtualMachine vm,

View File

@ -1723,11 +1723,7 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic
return _volStateMachine.transitTo(vol, event, null, _volsDao);
}
@Override
@ActionEvent(eventType = EventTypes.EVENT_VOLUME_DESTROY, eventDescription = "destroying a volume")
public Volume destroyVolume(long volumeId, Account caller, boolean expunge, boolean forceExpunge) {
VolumeVO volume = retrieveAndValidateVolume(volumeId, caller);
public void validateDestroyVolume(Volume volume, Account caller, boolean expunge, boolean forceExpunge) {
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();
@ -1737,6 +1733,14 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic
} else if (volume.getState() == Volume.State.Allocated || volume.getState() == Volume.State.Uploaded) {
throw new InvalidParameterValueException("The volume in Allocated/Uploaded state can only be expunged not destroyed/recovered");
}
}
@Override
@ActionEvent(eventType = EventTypes.EVENT_VOLUME_DESTROY, eventDescription = "destroying a volume")
public Volume destroyVolume(long volumeId, Account caller, boolean expunge, boolean forceExpunge) {
VolumeVO volume = retrieveAndValidateVolume(volumeId, caller);
validateDestroyVolume(volume, caller, expunge, forceExpunge);
destroyVolumeIfPossible(volume);

View File

@ -18,6 +18,8 @@ package com.cloud.vm;
import static com.cloud.configuration.ConfigurationManagerImpl.VM_USERDATA_MAX_LENGTH;
import static com.cloud.utils.NumbersUtil.toHumanReadableSize;
import static org.apache.cloudstack.api.ApiConstants.MAX_IOPS;
import static org.apache.cloudstack.api.ApiConstants.MIN_IOPS;
import java.io.IOException;
import java.io.StringReader;
@ -568,6 +570,8 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
@Inject
private VmStatsDao vmStatsDao;
@Inject
private DataCenterDao dataCenterDao;
@Inject
private MessageBus messageBus;
@Inject
protected CommandSetupHelper commandSetupHelper;
@ -2194,11 +2198,11 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
Long maxIopsInNewDiskOffering = null;
boolean autoMigrate = false;
boolean shrinkOk = false;
if (customParameters.containsKey(ApiConstants.MIN_IOPS)) {
minIopsInNewDiskOffering = Long.parseLong(customParameters.get(ApiConstants.MIN_IOPS));
if (customParameters.containsKey(MIN_IOPS)) {
minIopsInNewDiskOffering = Long.parseLong(customParameters.get(MIN_IOPS));
}
if (customParameters.containsKey(ApiConstants.MAX_IOPS)) {
minIopsInNewDiskOffering = Long.parseLong(customParameters.get(ApiConstants.MAX_IOPS));
if (customParameters.containsKey(MAX_IOPS)) {
minIopsInNewDiskOffering = Long.parseLong(customParameters.get(MAX_IOPS));
}
if (customParameters.containsKey(ApiConstants.AUTO_MIGRATE)) {
autoMigrate = Boolean.parseBoolean(customParameters.get(ApiConstants.AUTO_MIGRATE));
@ -3301,7 +3305,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
ServiceOfferingVO offering = serviceOfferingDao.findById(vmInstance.getId(), serviceOfferingId);
if (offering != null && offering.getRemoved() == null) {
if (offering.isVolatileVm()) {
return restoreVMInternal(caller, vmInstance, null);
return restoreVMInternal(caller, vmInstance);
}
} else {
throw new InvalidParameterValueException("Unable to find service offering: " + serviceOfferingId + " corresponding to the vm");
@ -6451,8 +6455,8 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
// if specified, minIops should be <= maxIops
private void verifyDetails(Map<String,String> details) {
if (details != null) {
String minIops = details.get("minIops");
String maxIops = details.get("maxIops");
String minIops = details.get(MIN_IOPS);
String maxIops = details.get(MAX_IOPS);
verifyMinAndMaxIops(minIops, maxIops);
@ -7811,6 +7815,20 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
return false;
}
private DiskOfferingVO validateAndGetDiskOffering(Long diskOfferingId, UserVmVO vm, Account caller) {
DiskOfferingVO diskOffering = _diskOfferingDao.findById(diskOfferingId);
if (diskOffering == null) {
throw new InvalidParameterValueException("Cannot find disk offering with ID " + diskOfferingId);
}
DataCenterVO zone = dataCenterDao.findById(vm.getDataCenterId());
_accountMgr.checkAccess(caller, diskOffering, zone);
ServiceOfferingVO serviceOffering = serviceOfferingDao.findById(vm.getServiceOfferingId());
if (serviceOffering.getDiskOfferingStrictness() && !serviceOffering.getDiskOfferingId().equals(diskOfferingId)) {
throw new InvalidParameterValueException("VM's service offering has a strict disk offering requirement, and the specified disk offering does not match");
}
return diskOffering;
}
@Override
public UserVm restoreVM(RestoreVMCmd cmd) throws InsufficientCapacityException, ResourceUnavailableException {
// Input validation
@ -7818,6 +7836,11 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
long vmId = cmd.getVmId();
Long newTemplateId = cmd.getTemplateId();
Long rootDiskOfferingId = cmd.getRootDiskOfferingId();
boolean expunge = cmd.getExpungeRootDisk();
Map<String, String> details = cmd.getDetails();
verifyDetails(details);
UserVmVO vm = _vmDao.findById(vmId);
if (vm == null) {
@ -7825,20 +7848,38 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
ex.addProxyObject(String.valueOf(vmId), "vmId");
throw ex;
}
_accountMgr.checkAccess(caller, null, true, vm);
DiskOffering diskOffering = rootDiskOfferingId != null ? validateAndGetDiskOffering(rootDiskOfferingId, vm, caller) : null;
VMTemplateVO template = _templateDao.findById(newTemplateId);
if (template.getSize() != null) {
String rootDiskSize = details.get(VmDetailConstants.ROOT_DISK_SIZE);
Long templateSize = template.getSize();
if (StringUtils.isNumeric(rootDiskSize)) {
if (Long.parseLong(rootDiskSize) * GiB_TO_BYTES < templateSize) {
throw new InvalidParameterValueException(String.format("Root disk size [%s] is smaller than the template size [%s]", rootDiskSize, templateSize));
}
} else if (diskOffering != null && diskOffering.getDiskSize() < templateSize) {
throw new InvalidParameterValueException(String.format("Disk size for selected offering [%s] is less than the template's size [%s]", diskOffering.getDiskSize(), templateSize));
}
}
//check if there are any active snapshots on volumes associated with the VM
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)) {
throw new CloudRuntimeException("There is/are unbacked up snapshot(s) on ROOT volume, Re-install VM is not permitted, please try again later.");
}
logger.debug("Found no ongoing snapshots on volume of type ROOT, for the vm with id " + vmId);
return restoreVMInternal(caller, vm, newTemplateId);
return restoreVMInternal(caller, vm, newTemplateId, rootDiskOfferingId, expunge, details);
}
public UserVm restoreVMInternal(Account caller, UserVmVO vm, Long newTemplateId) throws InsufficientCapacityException, ResourceUnavailableException {
return _itMgr.restoreVirtualMachine(vm.getId(), newTemplateId);
public UserVm restoreVMInternal(Account caller, UserVmVO vm, Long newTemplateId, Long rootDiskOfferingId, boolean expunge, Map<String, String> details) throws InsufficientCapacityException, ResourceUnavailableException {
return _itMgr.restoreVirtualMachine(vm.getId(), newTemplateId, rootDiskOfferingId, expunge, details);
}
public UserVm restoreVMInternal(Account caller, UserVmVO vm) throws InsufficientCapacityException, ResourceUnavailableException {
return restoreVMInternal(caller, vm, null, null, false, null);
}
private VMTemplateVO getRestoreVirtualMachineTemplate(Account caller, Long newTemplateId, List<VolumeVO> rootVols, UserVmVO vm) {
@ -7883,7 +7924,9 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
}
@Override
public UserVm restoreVirtualMachine(final Account caller, final long vmId, final Long newTemplateId) throws InsufficientCapacityException, ResourceUnavailableException {
public UserVm restoreVirtualMachine(final Account caller, final long vmId, final Long newTemplateId,
final Long rootDiskOfferingId,
final boolean expunge, final Map<String, String> details) throws InsufficientCapacityException, ResourceUnavailableException {
Long userId = caller.getId();
_userDao.findById(userId);
UserVmVO vm = _vmDao.findById(vmId);
@ -7940,9 +7983,10 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
}
}
List<Volume> newVols = new ArrayList<>();
DiskOffering diskOffering = rootDiskOfferingId != null ? _diskOfferingDao.findById(rootDiskOfferingId) : null;
for (VolumeVO root : rootVols) {
if ( !Volume.State.Allocated.equals(root.getState()) || newTemplateId != null ) {
_volumeService.validateDestroyVolume(root, caller, expunge, false);
final UserVmVO userVm = vm;
Pair<UserVmVO, Volume> vmAndNewVol = Transaction.execute(new TransactionCallbackWithException<Pair<UserVmVO, Volume>, CloudRuntimeException>() {
@Override
@ -7973,15 +8017,9 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
} else {
newVol = volumeMgr.allocateDuplicateVolume(root, null);
}
newVols.add(newVol);
if (userVmDetailsDao.findDetail(userVm.getId(), VmDetailConstants.ROOT_DISK_SIZE) == null && !newVol.getSize().equals(template.getSize())) {
VolumeVO resizedVolume = (VolumeVO) newVol;
if (template.getSize() != null) {
resizedVolume.setSize(template.getSize());
_volsDao.update(resizedVolume.getId(), resizedVolume);
}
}
updateVolume(newVol, template, userVm, diskOffering, details);
volumeMgr.saveVolumeDetails(newVol.getDiskOfferingId(), newVol.getId());
// 1. Save usage event and update resource count for user vm volumes
try {
@ -8010,7 +8048,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
// Detach, destroy and create the usage event for the old root volume.
_volsDao.detachVolume(root.getId());
volumeMgr.destroyVolume(root);
_volumeService.destroyVolume(root.getId(), caller, expunge, false);
// For VMware hypervisor since the old root volume is replaced by the new root volume, force expunge old root volume if it has been created in storage
if (vm.getHypervisorType() == HypervisorType.VMware) {
@ -8073,6 +8111,48 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
}
private void updateVolume(Volume vol, VMTemplateVO template, UserVmVO userVm, DiskOffering diskOffering, Map<String, String> details) {
VolumeVO resizedVolume = (VolumeVO) vol;
if (userVmDetailsDao.findDetail(userVm.getId(), VmDetailConstants.ROOT_DISK_SIZE) == null && !vol.getSize().equals(template.getSize())) {
if (template.getSize() != null) {
resizedVolume.setSize(template.getSize());
}
}
if (diskOffering != null) {
resizedVolume.setDiskOfferingId(diskOffering.getId());
resizedVolume.setSize(diskOffering.getDiskSize());
if (diskOffering.isCustomized()) {
resizedVolume.setSize(vol.getSize());
}
if (diskOffering.getMinIops() != null) {
resizedVolume.setMinIops(diskOffering.getMinIops());
}
if (diskOffering.getMaxIops() != null) {
resizedVolume.setMaxIops(diskOffering.getMaxIops());
}
}
if (MapUtils.isNotEmpty(details)) {
if (StringUtils.isNumeric(details.get(VmDetailConstants.ROOT_DISK_SIZE))) {
Long rootDiskSize = Long.parseLong(details.get(VmDetailConstants.ROOT_DISK_SIZE)) * GiB_TO_BYTES;
resizedVolume.setSize(rootDiskSize);
}
String minIops = details.get(MIN_IOPS);
String maxIops = details.get(MAX_IOPS);
if (StringUtils.isNumeric(minIops)) {
resizedVolume.setMinIops(Long.parseLong(minIops));
}
if (StringUtils.isNumeric(maxIops)) {
resizedVolume.setMinIops(Long.parseLong(maxIops));
}
}
_volsDao.update(resizedVolume.getId(), resizedVolume);
}
private void updateVMDynamicallyScalabilityUsingTemplate(UserVmVO vm, Long newTemplateId) {
ServiceOfferingVO serviceOffering = serviceOfferingDao.findById(vm.getServiceOfferingId());
VMTemplateVO newTemplate = _templateDao.findById(newTemplateId);

View File

@ -185,6 +185,9 @@ import java.util.Random;
import java.util.Set;
import java.util.stream.Collectors;
import static org.apache.cloudstack.api.ApiConstants.MAX_IOPS;
import static org.apache.cloudstack.api.ApiConstants.MIN_IOPS;
public class UnmanagedVMsManagerImpl implements UnmanagedVMsManager {
public static final String VM_IMPORT_DEFAULT_TEMPLATE_NAME = "system-default-vm-import-dummy-template.iso";
public static final String KVM_VM_IMPORT_DEFAULT_TEMPLATE_NAME = "kvm-default-vm-import-dummy-template";
@ -1200,12 +1203,12 @@ public class UnmanagedVMsManagerImpl implements UnmanagedVMsManager {
throw new InvalidParameterValueException(String.format("Root disk ID: %s size is invalid", rootDisk.getDiskId()));
}
Long minIops = null;
if (details.containsKey("minIops")) {
minIops = Long.parseLong(details.get("minIops"));
if (details.containsKey(MIN_IOPS)) {
minIops = Long.parseLong(details.get(MIN_IOPS));
}
Long maxIops = null;
if (details.containsKey("maxIops")) {
maxIops = Long.parseLong(details.get("maxIops"));
if (details.containsKey(MAX_IOPS)) {
maxIops = Long.parseLong(details.get(MAX_IOPS));
}
DiskOfferingVO diskOffering = diskOfferingDao.findById(serviceOffering.getDiskOfferingId());
diskProfileStoragePoolList.add(importDisk(rootDisk, userVm, cluster, diskOffering, Volume.Type.ROOT, String.format("ROOT-%d", userVm.getId()),

View File

@ -97,8 +97,6 @@ import com.cloud.service.dao.ServiceOfferingDao;
import com.cloud.storage.DiskOfferingVO;
import com.cloud.storage.GuestOSVO;
import com.cloud.storage.ScopeType;
import com.cloud.storage.Snapshot;
import com.cloud.storage.SnapshotVO;
import com.cloud.storage.Storage;
import com.cloud.storage.VMTemplateVO;
import com.cloud.storage.Volume;
@ -1321,18 +1319,6 @@ public class UserVmManagerImplTest {
when(cmd.getTemplateId()).thenReturn(2L);
when(userVmDao.findById(vmId)).thenReturn(userVmVoMock);
List<VolumeVO> volumes = new ArrayList<>();
long rootVolumeId = 1l;
VolumeVO rootVolumeOfVm = Mockito.mock(VolumeVO.class);
Mockito.when(rootVolumeOfVm.getId()).thenReturn(rootVolumeId);
volumes.add(rootVolumeOfVm);
when(volumeDaoMock.findByInstanceAndType(vmId, Volume.Type.ROOT)).thenReturn(volumes);
List<SnapshotVO> snapshots = new ArrayList<>();
SnapshotVO snapshot = Mockito.mock(SnapshotVO.class);
snapshots.add(snapshot);
when(snapshotDaoMock.listByStatus(rootVolumeId, Snapshot.State.Creating, Snapshot.State.CreatedOnPrimary, Snapshot.State.BackingUp)).thenReturn(snapshots);
userVmManagerImpl.restoreVM(cmd);
}
@ -1346,7 +1332,7 @@ public class UserVmManagerImplTest {
when(userVmVoMock.getAccountId()).thenReturn(accountId);
when(accountDao.findById(accountId)).thenReturn(null);
userVmManagerImpl.restoreVirtualMachine(accountMock, vmId, newTemplateId);
userVmManagerImpl.restoreVirtualMachine(accountMock, vmId, newTemplateId, null, false, null);
}
@Test(expected = PermissionDeniedException.class)
@ -1360,7 +1346,7 @@ public class UserVmManagerImplTest {
when(accountDao.findById(accountId)).thenReturn(callerAccount);
when(callerAccount.getState()).thenReturn(Account.State.DISABLED);
userVmManagerImpl.restoreVirtualMachine(accountMock, vmId, newTemplateId);
userVmManagerImpl.restoreVirtualMachine(accountMock, vmId, newTemplateId, null, false, null);
}
@Test(expected = CloudRuntimeException.class)
@ -1375,7 +1361,7 @@ public class UserVmManagerImplTest {
when(accountDao.findById(accountId)).thenReturn(callerAccount);
when(userVmVoMock.getState()).thenReturn(VirtualMachine.State.Starting);
userVmManagerImpl.restoreVirtualMachine(accountMock, vmId, newTemplateId);
userVmManagerImpl.restoreVirtualMachine(accountMock, vmId, newTemplateId, null, false, null);
}
@Test(expected = InvalidParameterValueException.class)
@ -1396,7 +1382,7 @@ public class UserVmManagerImplTest {
when(templateDao.findById(currentTemplateId)).thenReturn(currentTemplate);
when(volumeDaoMock.findByInstanceAndType(vmId, Volume.Type.ROOT)).thenReturn(new ArrayList<VolumeVO>());
userVmManagerImpl.restoreVirtualMachine(accountMock, vmId, newTemplateId);
userVmManagerImpl.restoreVirtualMachine(accountMock, vmId, newTemplateId, null, false, null);
}
@Test(expected = InvalidParameterValueException.class)
@ -1423,7 +1409,7 @@ public class UserVmManagerImplTest {
volumes.add(rootVolume2);
when(volumeDaoMock.findByInstanceAndType(vmId, Volume.Type.ROOT)).thenReturn(volumes);
userVmManagerImpl.restoreVirtualMachine(accountMock, vmId, newTemplateId);
userVmManagerImpl.restoreVirtualMachine(accountMock, vmId, newTemplateId, null, false, null);
}
@Test(expected = InvalidParameterValueException.class)
@ -1450,7 +1436,7 @@ public class UserVmManagerImplTest {
vmSnapshots.add(vmSnapshot);
when(vmSnapshotDaoMock.findByVm(vmId)).thenReturn(vmSnapshots);
userVmManagerImpl.restoreVirtualMachine(accountMock, vmId, newTemplateId);
userVmManagerImpl.restoreVirtualMachine(accountMock, vmId, newTemplateId, null, false, null);
}
@Test

View File

@ -164,33 +164,10 @@ export default {
label: 'label.reinstall.vm',
message: 'message.reinstall.vm',
dataView: true,
args: ['virtualmachineid', 'templateid'],
filters: (record) => {
var filters = {}
var filterParams = {}
filterParams.hypervisortype = record.hypervisor
filterParams.zoneid = record.zoneid
filters.templateid = filterParams
return filters
},
popup: true,
show: (record) => { return ['Running', 'Stopped'].includes(record.state) },
mapping: {
virtualmachineid: {
value: (record) => { return record.id }
}
},
disabled: (record) => { return record.hostcontrolstate === 'Offline' },
successMethod: (obj, result) => {
const vm = result.jobresult.virtualmachine || {}
if (result.jobstatus === 1 && vm.password) {
const name = vm.displayname || vm.name || vm.id
obj.$notification.success({
message: `${obj.$t('label.reinstall.vm')}: ` + name,
description: `${obj.$t('label.password.reset.confirm')}: ` + vm.password,
duration: 0
})
}
}
component: shallowRef(defineAsyncComponent(() => import('@/views/compute/ReinstallVm.vue')))
},
{
api: 'createVMSnapshot',

View File

@ -0,0 +1,307 @@
// 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-form
v-ctrl-enter="handleSubmit"
@finish="handleSubmit"
layout="vertical"
>
<a-alert
type="warning"
show-icon
>
<template #message><span
style="margin-bottom: 5px"
v-html="$t('message.reinstall.vm')"
/></template>
</a-alert>
<a-form-item>
<template-iso-selection
input-decorator="templateid"
:items="templates"
:selected="tabKey"
:loading="loading.templates"
:preFillContent="resource.templateid"
:key="templateKey"
@handle-search-filter="($event) => fetchAllTemplates($event)"
@update-template-iso="updateFieldValue"
/>
</a-form-item>
<a-form-item>
<template #label>
<tooltip-label
:title="$t('label.override.root.diskoffering')"
:tooltip="apiParams.diskofferingid.description"
/>
</template>
<a-switch
v-model:checked="overrideDiskOffering"
@change="val => { overrideDiskOffering = val }"
/>
</a-form-item>
<a-form-item v-if="overrideDiskOffering">
<disk-offering-selection
:items="diskOfferings"
:row-count="diskOfferingCount"
:zoneId="resource.zoneId"
:value="diskOffering ? diskOffering.id : ''"
:loading="loading.diskOfferings"
:preFillContent="resource.diskofferingid"
:isIsoSelected="false"
:isRootDiskOffering="true"
@on-selected-disk-size="onSelectDiskSize"
@handle-search-filter="($event) => fetchDiskOfferings($event)"
/>
</a-form-item>
<a-form-item v-if="diskOffering && (diskOffering.iscustomized || diskOffering.iscustomizediops)">
<disk-size-selection
input-decorator="rootdisksize"
:diskSelected="diskOffering"
:isCustomized="diskOffering.iscustomized"
@handler-error="handlerError"
@update-disk-size="updateFieldValue"
@update-root-disk-iops-value="updateFieldValue"
/>
</a-form-item>
<a-form-item v-if="!(diskOffering && diskOffering.iscustomized)">
<template #label>
<tooltip-label
:title="$t('label.override.rootdisk.size')"
:tooltip="apiParams.rootdisksize.description"
/>
</template>
<a-switch
v-model:checked="overrideDiskSize"
@change="val => { overrideDiskSize = val }"
/>
<disk-size-selection
v-if="overrideDiskSize"
input-decorator="rootdisksize"
:isCustomized="true"
@update-disk-size="(input, value) => updateFieldValue('overrideRootDiskSize', value)"
style="margin-top: 10px;"
/>
</a-form-item>
<a-form-item>
<template #label>
<tooltip-label
:title="$t('label.expunge')"
:tooltip="apiParams.expunge.description"
/>
</template>
<a-switch
v-model:checked="expungeDisk"
@change="val => { expungeDisk = val }"
/>
</a-form-item>
<div
:span="24"
class="action-button"
>
<a-button @click="closeAction">{{ $t('label.cancel') }}</a-button>
<a-button
ref="submit"
type="primary"
@click="handleSubmit"
>{{ $t('label.ok') }}</a-button>
</div>
</a-form>
</template>
<script>
import { api } from '@/api'
import DiskOfferingSelection from '@views/compute/wizard/DiskOfferingSelection'
import DiskSizeSelection from '@views/compute/wizard/DiskSizeSelection'
import TemplateIsoSelection from '@views/compute/wizard/TemplateIsoSelection'
import TooltipLabel from '@/components/widgets/TooltipLabel'
import _ from 'lodash'
export default {
name: 'ReinstallVM',
components: {
DiskOfferingSelection,
DiskSizeSelection,
TemplateIsoSelection,
TooltipLabel
},
props: {
resource: {
type: Object,
required: true
}
},
inject: ['parentFetchData'],
data () {
return {
overrideDiskOffering: false,
overrideDiskSize: false,
expungeDisk: false,
selectedDiskOffering: {},
loading: {
templates: false,
diskOfferings: false
},
rootDiskSizeKey: 'details[0].rootdisksize',
minIopsKey: 'details[0].minIops',
maxIopsKey: 'details[0].maxIops',
rootdisksize: 0,
minIops: 0,
maxIops: 0,
templateFilter: [
'featured',
'community',
'selfexecutable',
'sharedexecutable'
],
diskOffering: {},
diskOfferingCount: 0,
templateKey: 0
}
},
beforeCreate () {
this.apiParams = this.$getApiParams('restoreVirtualMachine')
},
created () {
this.fetchData()
},
methods: {
fetchData () {
this.fetchDiskOfferings({})
this.fetchAllTemplates()
},
closeAction () {
this.$emit('close-action')
},
handlerError (error) {
this.error = error
},
handleSubmit () {
const params = {
virtualmachineid: this.resource.id,
templateid: this.templateid
}
if (this.overrideDiskOffering) {
params.diskofferingid = this.diskOffering.id
if (this.diskOffering.iscustomized) {
params[this.rootDiskSizeKey] = this.rootdisksize
}
if (this.diskOffering.iscustomizediops) {
params[this.minIopsKey] = this.minIops
params[this.maxIopsKey] = this.maxIops
}
}
if (this.overrideDiskSize && this.overrideRootDiskSize) {
params.rootdisksize = this.overrideRootDiskSize
}
params.expunge = this.expungeDisk
api('restoreVirtualMachine', params).then(response => {
this.$pollJob({
jobId: response.restorevmresponse.jobid,
successMessage: this.$t('label.reinstall.vm') + ' ' + this.$t('label.success'),
successMethod: (result) => {
const vm = result.jobresult.virtualmachine || {}
const name = vm.displayname || vm.name || vm.id
if (result.jobstatus === 1 && vm.password) {
this.$notification.success({
message: `${this.$t('label.reinstall.vm')}: ` + name,
description: `${this.$t('label.password.reset.confirm')}: ` + vm.password,
duration: 0
})
}
},
errorMessage: this.$t('label.reinstall.vm') + ' ' + this.$t('label.failed'),
errorMethod: (result) => {
this.closeAction()
},
loadingMessage: this.$t('label.reinstall.vm') + ': ' + this.resource.name,
catchMessage: this.$t('error.fetching.async.job.result')
})
}).catch(error => {
this.$notifyError(error)
this.closeAction()
}).finally(() => {
this.closeAction()
})
},
fetchAllTemplates (params) {
const promises = []
const templates = {}
this.loading.templates = true
this.templateFilter.forEach((filter) => {
templates[filter] = { count: 0, template: [] }
promises.push(this.fetchTemplates(filter, params))
})
this.templates = templates
Promise.all(promises).then((response) => {
response.forEach((resItem, idx) => {
templates[this.templateFilter[idx]] = _.isEmpty(resItem.listtemplatesresponse) ? { count: 0, template: [] } : resItem.listtemplatesresponse
this.templates = { ...templates }
})
}).catch((reason) => {
console.log(reason)
}).finally(() => {
this.loading.templates = false
})
},
fetchTemplates (templateFilter, params) {
const args = Object.assign({}, params)
if (args.keyword || args.category !== templateFilter) {
args.page = 1
args.pageSize = args.pageSize || 10
}
args.zoneid = _.get(this.zone, 'id')
args.templatefilter = templateFilter
args.details = 'all'
args.showicon = 'true'
return new Promise((resolve, reject) => {
api('listTemplates', args).then((response) => {
resolve(response)
}).catch((reason) => {
reject(reason)
})
})
},
fetchDiskOfferings (params) {
api('listDiskOfferings', { zoneid: this.resource.zoneid, listall: true, ...params }).then((response) => {
this.diskOfferings = response?.listdiskofferingsresponse?.diskoffering || []
this.diskOfferingCount = response?.listdiskofferingsresponse?.count || 0
})
},
onSelectDiskSize (rowSelected) {
this.diskOffering = rowSelected
},
updateFieldValue (input, value) {
this[input] = value
}
}
}
</script>
<style
scoped
lang="scss"
>
.ant-form {
width: 90vw;
@media (min-width: 700px) {
width: 50vw;
}
}
</style>