mirror of https://github.com/apache/cloudstack.git
[Veeam] Block operations in restoring VMs (#7238)
Co-authored-by: SadiJr <sadi@scclouds.com.br>
This commit is contained in:
parent
57ff125f83
commit
1e253401b0
|
|
@ -57,7 +57,8 @@ public interface Volume extends ControlledEntity, Identity, InternalIdentity, Ba
|
|||
UploadInProgress("Volume upload is in progress"),
|
||||
UploadError("Volume upload encountered some error"),
|
||||
UploadAbandoned("Volume upload is abandoned since the upload was never initiated within a specified time"),
|
||||
Attaching("The volume is attaching to a VM from Ready state.");
|
||||
Attaching("The volume is attaching to a VM from Ready state."),
|
||||
Restoring("The volume is being restored from backup.");
|
||||
|
||||
String _description;
|
||||
|
||||
|
|
@ -133,6 +134,11 @@ public interface Volume extends ControlledEntity, Identity, InternalIdentity, Ba
|
|||
s_fsm.addTransition(new StateMachine2.Transition<State, Event>(Attaching, Event.OperationSucceeded, Ready, null));
|
||||
s_fsm.addTransition(new StateMachine2.Transition<State, Event>(Attaching, Event.OperationFailed, Ready, null));
|
||||
s_fsm.addTransition(new StateMachine2.Transition<State, Event>(Destroy, Event.RecoverRequested, Ready, null));
|
||||
s_fsm.addTransition(new StateMachine2.Transition<State, Event>(Ready, Event.RestoreRequested, Restoring, null));
|
||||
s_fsm.addTransition(new StateMachine2.Transition<State, Event>(Expunged, Event.RestoreRequested, Restoring, null));
|
||||
s_fsm.addTransition(new StateMachine2.Transition<State, Event>(Destroy, Event.RestoreRequested, Restoring, null));
|
||||
s_fsm.addTransition(new StateMachine2.Transition<State, Event>(Restoring, Event.RestoreSucceeded, Ready, null));
|
||||
s_fsm.addTransition(new StateMachine2.Transition<State, Event>(Restoring, Event.RestoreFailed, Ready, null));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -156,7 +162,10 @@ public interface Volume extends ControlledEntity, Identity, InternalIdentity, Ba
|
|||
ExpungingRequested,
|
||||
ResizeRequested,
|
||||
AttachRequested,
|
||||
OperationTimeout;
|
||||
OperationTimeout,
|
||||
RestoreRequested,
|
||||
RestoreSucceeded,
|
||||
RestoreFailed;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ package com.cloud.storage;
|
|||
import java.net.MalformedURLException;
|
||||
import java.util.Map;
|
||||
|
||||
import com.cloud.utils.fsm.NoTransitionException;
|
||||
import org.apache.cloudstack.api.command.user.volume.AssignVolumeCmd;
|
||||
import org.apache.cloudstack.api.command.user.volume.AttachVolumeCmd;
|
||||
import org.apache.cloudstack.api.command.user.volume.ChangeOfferingForVolumeCmd;
|
||||
|
|
@ -172,4 +173,6 @@ public interface VolumeApiService {
|
|||
Volume changeDiskOfferingForVolume(ChangeOfferingForVolumeCmd cmd) throws ResourceAllocationException;
|
||||
|
||||
void publishVolumeCreationUsageEvent(Volume volume);
|
||||
|
||||
boolean stateTransitTo(Volume vol, Volume.Event event) throws NoTransitionException;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,7 +57,8 @@ public interface VirtualMachine extends RunningOn, ControlledEntity, Partition,
|
|||
Migrating(true, "VM is being migrated. host id holds to from host"),
|
||||
Error(false, "VM is in error"),
|
||||
Unknown(false, "VM state is unknown."),
|
||||
Shutdown(false, "VM state is shutdown from inside");
|
||||
Shutdown(false, "VM state is shutdown from inside"),
|
||||
Restoring(true, "VM is being restored from backup");
|
||||
|
||||
private final boolean _transitional;
|
||||
String _description;
|
||||
|
|
@ -126,6 +127,11 @@ public interface VirtualMachine extends RunningOn, ControlledEntity, Partition,
|
|||
s_fsm.addTransition(new Transition<State, Event>(State.Expunging, VirtualMachine.Event.ExpungeOperation, State.Expunging,null));
|
||||
s_fsm.addTransition(new Transition<State, Event>(State.Error, VirtualMachine.Event.DestroyRequested, State.Expunging, null));
|
||||
s_fsm.addTransition(new Transition<State, Event>(State.Error, VirtualMachine.Event.ExpungeOperation, State.Expunging, null));
|
||||
s_fsm.addTransition(new Transition<State, Event>(State.Stopped, Event.RestoringRequested, State.Restoring, null));
|
||||
s_fsm.addTransition(new Transition<State, Event>(State.Expunging, Event.RestoringRequested, State.Restoring, null));
|
||||
s_fsm.addTransition(new Transition<State, Event>(State.Destroyed, Event.RestoringRequested, State.Restoring, null));
|
||||
s_fsm.addTransition(new Transition<State, Event>(State.Restoring, Event.RestoringSuccess, State.Stopped, null));
|
||||
s_fsm.addTransition(new Transition<State, Event>(State.Restoring, Event.RestoringFailed, State.Stopped, null));
|
||||
|
||||
s_fsm.addTransition(new Transition<State, Event>(State.Starting, VirtualMachine.Event.FollowAgentPowerOnReport, State.Running, Arrays.asList(new Impact[]{Impact.USAGE})));
|
||||
s_fsm.addTransition(new Transition<State, Event>(State.Stopping, VirtualMachine.Event.FollowAgentPowerOnReport, State.Running, null));
|
||||
|
|
@ -210,6 +216,9 @@ public interface VirtualMachine extends RunningOn, ControlledEntity, Partition,
|
|||
AgentReportMigrated,
|
||||
RevertRequested,
|
||||
SnapshotRequested,
|
||||
RestoringRequested,
|
||||
RestoringFailed,
|
||||
RestoringSuccess,
|
||||
|
||||
// added for new VMSync logic
|
||||
FollowAgentPowerOnReport,
|
||||
|
|
|
|||
|
|
@ -978,16 +978,12 @@ public class CapacityManagerImpl extends ManagerBase implements CapacityManager,
|
|||
allocateVmCapacity(vm, fromLastHost);
|
||||
}
|
||||
|
||||
if (newState == State.Stopped) {
|
||||
if (vm.getType() == VirtualMachine.Type.User) {
|
||||
|
||||
if (newState == State.Stopped && event != Event.RestoringFailed && event != Event.RestoringSuccess && vm.getType() == VirtualMachine.Type.User) {
|
||||
UserVmVO userVM = _userVMDao.findById(vm.getId());
|
||||
_userVMDao.loadDetails(userVM);
|
||||
// free the message sent flag if it exists
|
||||
userVM.setDetail(VmDetailConstants.MESSAGE_RESERVED_CAPACITY_FREED_FLAG, "false");
|
||||
_userVMDao.saveDetails(userVM);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -1655,7 +1655,7 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic
|
|||
}
|
||||
}
|
||||
|
||||
protected boolean stateTransitTo(Volume vol, Volume.Event event) throws NoTransitionException {
|
||||
public boolean stateTransitTo(Volume vol, Volume.Event event) throws NoTransitionException {
|
||||
return _volStateMachine.transitTo(vol, event, null, _volsDao);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,9 @@ import java.util.TimeZone;
|
|||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
|
||||
import com.cloud.storage.VolumeApiService;
|
||||
import com.cloud.utils.fsm.NoTransitionException;
|
||||
import com.cloud.vm.VirtualMachineManager;
|
||||
import javax.inject.Inject;
|
||||
import javax.naming.ConfigurationException;
|
||||
|
||||
|
|
@ -53,6 +56,7 @@ import org.apache.cloudstack.backup.dao.BackupDao;
|
|||
import org.apache.cloudstack.backup.dao.BackupOfferingDao;
|
||||
import org.apache.cloudstack.backup.dao.BackupScheduleDao;
|
||||
import org.apache.cloudstack.context.CallContext;
|
||||
import org.apache.cloudstack.engine.orchestration.service.VolumeOrchestrationService;
|
||||
import org.apache.cloudstack.framework.config.ConfigKey;
|
||||
import org.apache.cloudstack.framework.jobs.AsyncJobDispatcher;
|
||||
import org.apache.cloudstack.framework.jobs.AsyncJobManager;
|
||||
|
|
@ -147,6 +151,12 @@ public class BackupManagerImpl extends ManagerBase implements BackupManager {
|
|||
private ApiDispatcher apiDispatcher;
|
||||
@Inject
|
||||
private AsyncJobManager asyncJobManager;
|
||||
@Inject
|
||||
private VirtualMachineManager virtualMachineManager;
|
||||
@Inject
|
||||
private VolumeApiService volumeApiService;
|
||||
@Inject
|
||||
private VolumeOrchestrationService volumeOrchestrationService;
|
||||
|
||||
private AsyncJobDispatcher asyncJobDispatcher;
|
||||
private Timer backupTimer;
|
||||
|
|
@ -585,17 +595,100 @@ public class BackupManagerImpl extends ManagerBase implements BackupManager {
|
|||
|
||||
final BackupOffering offering = backupOfferingDao.findByIdIncludingRemoved(vm.getBackupOfferingId());
|
||||
if (offering == null) {
|
||||
throw new CloudRuntimeException("Failed to find backup offering of the VM backup");
|
||||
throw new CloudRuntimeException("Failed to find backup offering of the VM backup.");
|
||||
}
|
||||
|
||||
final BackupProvider backupProvider = getBackupProvider(offering.getProvider());
|
||||
if (!backupProvider.restoreVMFromBackup(vm, backup)) {
|
||||
throw new CloudRuntimeException("Error restoring VM from backup ID " + backup.getId());
|
||||
}
|
||||
String backupDetailsInMessage = ReflectionToStringBuilderUtils.reflectOnlySelectedFields(backup, "uuid", "externalId", "vmId", "type", "status", "date");
|
||||
tryRestoreVM(backup, vm, offering, backupDetailsInMessage);
|
||||
updateVolumeState(vm, Volume.Event.RestoreSucceeded, Volume.State.Ready);
|
||||
updateVmState(vm, VirtualMachine.Event.RestoringSuccess, VirtualMachine.State.Stopped);
|
||||
|
||||
return importRestoredVM(vm.getDataCenterId(), vm.getDomainId(), vm.getAccountId(), vm.getUserId(),
|
||||
vm.getInstanceName(), vm.getHypervisorType(), backup);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to restore a VM from a backup. <br/>
|
||||
* First update the VM state to {@link VirtualMachine.Event#RestoringRequested} and its volume states to {@link Volume.Event#RestoreRequested}, <br/>
|
||||
* and then try to restore the backup. <br/>
|
||||
*
|
||||
* If restore fails, then update the VM state to {@link VirtualMachine.Event#RestoringFailed}, and its volumes to {@link Volume.Event#RestoreFailed} and throw an {@link CloudRuntimeException}.
|
||||
*/
|
||||
protected void tryRestoreVM(BackupVO backup, VMInstanceVO vm, BackupOffering offering, String backupDetailsInMessage) {
|
||||
try {
|
||||
updateVmState(vm, VirtualMachine.Event.RestoringRequested, VirtualMachine.State.Restoring);
|
||||
updateVolumeState(vm, Volume.Event.RestoreRequested, Volume.State.Restoring);
|
||||
final BackupProvider backupProvider = getBackupProvider(offering.getProvider());
|
||||
if (!backupProvider.restoreVMFromBackup(vm, backup)) {
|
||||
throw new CloudRuntimeException(String.format("Error restoring %s from backup [%s].", vm, backupDetailsInMessage));
|
||||
}
|
||||
// The restore process is executed by a backup provider outside of ACS, I am using the catch-all (Exception) to
|
||||
// ensure that no provider-side exception is missed. Therefore, we have a proper handling of exceptions, and rollbacks if needed.
|
||||
} catch (Exception e) {
|
||||
LOG.error(String.format("Failed to restore backup [%s] due to: [%s].", backupDetailsInMessage, e.getMessage()), e);
|
||||
updateVolumeState(vm, Volume.Event.RestoreFailed, Volume.State.Ready);
|
||||
updateVmState(vm, VirtualMachine.Event.RestoringFailed, VirtualMachine.State.Stopped);
|
||||
throw new CloudRuntimeException(String.format("Error restoring VM from backup [%s].", backupDetailsInMessage));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to update the state of given VM, given specified event
|
||||
* @param vm The VM to update its state
|
||||
* @param event The event to update the VM state
|
||||
* @param next The desired state, just needed to add more context to the logs
|
||||
*/
|
||||
private void updateVmState(VMInstanceVO vm, VirtualMachine.Event event, VirtualMachine.State next) {
|
||||
LOG.debug(String.format("Trying to update state of VM [%s] with event [%s].", vm, event));
|
||||
Transaction.execute(TransactionLegacy.CLOUD_DB, (TransactionCallback<VMInstanceVO>) status -> {
|
||||
try {
|
||||
if (!virtualMachineManager.stateTransitTo(vm, event, vm.getHostId())) {
|
||||
throw new CloudRuntimeException(String.format("Unable to change state of VM [%s] to [%s].", vm, next));
|
||||
}
|
||||
} catch (NoTransitionException e) {
|
||||
String errMsg = String.format("Failed to update state of VM [%s] with event [%s] due to [%s].", vm, event, e.getMessage());
|
||||
LOG.error(errMsg, e);
|
||||
throw new RuntimeException(errMsg);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to update all volume states of given VM, given specified event
|
||||
* @param vm The VM to which the volumes belong
|
||||
* @param event The event to update the volume states
|
||||
* @param next The desired state, just needed to add more context to the logs
|
||||
*/
|
||||
private void updateVolumeState(VMInstanceVO vm, Volume.Event event, Volume.State next) {
|
||||
Transaction.execute(TransactionLegacy.CLOUD_DB, (TransactionCallback<VolumeVO>) status -> {
|
||||
for (VolumeVO volume : volumeDao.findIncludingRemovedByInstanceAndType(vm.getId(), null)) {
|
||||
tryToUpdateStateOfSpecifiedVolume(volume, event, next);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to update the state of just one volume using any passed {@link Volume.Event}. Throws an {@link RuntimeException} when fails.
|
||||
* @param volume The volume to update it state
|
||||
* @param event The event to update the volume state
|
||||
* @param next The desired state, just needed to add more context to the logs
|
||||
*
|
||||
*/
|
||||
private void tryToUpdateStateOfSpecifiedVolume(VolumeVO volume, Volume.Event event, Volume.State next) {
|
||||
LOG.debug(String.format("Trying to update state of volume [%s] with event [%s].", volume, event));
|
||||
try {
|
||||
if (!volumeApiService.stateTransitTo(volume, event)) {
|
||||
throw new CloudRuntimeException(String.format("Unable to change state of volume [%s] to [%s].", volume, next));
|
||||
}
|
||||
} catch (NoTransitionException e) {
|
||||
String errMsg = String.format("Failed to update state of volume [%s] with event [%s] due to [%s].", volume, event, e.getMessage());
|
||||
LOG.error(errMsg, e);
|
||||
throw new RuntimeException(errMsg);
|
||||
}
|
||||
}
|
||||
|
||||
private Backup.VolumeInfo getVolumeInfo(List<Backup.VolumeInfo> backedUpVolumes, String volumeUuid) {
|
||||
for (Backup.VolumeInfo volInfo : backedUpVolumes) {
|
||||
if (volInfo.getUuid().equals(volumeUuid)) {
|
||||
|
|
@ -652,16 +745,20 @@ public class BackupManagerImpl extends ManagerBase implements BackupManager {
|
|||
String[] hostPossibleValues = {host.getPrivateIpAddress(), host.getName()};
|
||||
String[] datastoresPossibleValues = {datastore.getUuid(), datastore.getName()};
|
||||
|
||||
updateVmState(vm, VirtualMachine.Event.RestoringRequested, VirtualMachine.State.Restoring);
|
||||
Pair<Boolean, String> result = restoreBackedUpVolume(backedUpVolumeUuid, backup, backupProvider, hostPossibleValues, datastoresPossibleValues);
|
||||
|
||||
if (BooleanUtils.isFalse(result.first())) {
|
||||
updateVmState(vm, VirtualMachine.Event.RestoringFailed, VirtualMachine.State.Stopped);
|
||||
throw new CloudRuntimeException(String.format("Error restoring volume [%s] of VM [%s] to host [%s] using backup provider [%s] due to: [%s].",
|
||||
backedUpVolumeUuid, vm.getUuid(), host.getUuid(), backupProvider.getName(), result.second()));
|
||||
}
|
||||
if (!attachVolumeToVM(vm.getDataCenterId(), result.second(), vmFromBackup.getBackupVolumeList(),
|
||||
backedUpVolumeUuid, vm, datastore.getUuid(), backup)) {
|
||||
updateVmState(vm, VirtualMachine.Event.RestoringFailed, VirtualMachine.State.Stopped);
|
||||
throw new CloudRuntimeException(String.format("Error attaching volume [%s] to VM [%s]." + backedUpVolumeUuid, vm.getUuid()));
|
||||
}
|
||||
updateVmState(vm, VirtualMachine.Event.RestoringSuccess, VirtualMachine.State.Stopped);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,9 +16,19 @@
|
|||
// under the License.
|
||||
package org.apache.cloudstack.backup;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.fail;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.cloud.storage.Volume;
|
||||
import com.cloud.storage.VolumeApiService;
|
||||
import com.cloud.storage.VolumeVO;
|
||||
import com.cloud.storage.dao.VolumeDao;
|
||||
import com.cloud.utils.exception.CloudRuntimeException;
|
||||
import com.cloud.utils.fsm.NoTransitionException;
|
||||
import com.cloud.vm.VMInstanceVO;
|
||||
import com.cloud.vm.VirtualMachine;
|
||||
import com.cloud.vm.VirtualMachineManager;
|
||||
import org.apache.cloudstack.api.ServerApiException;
|
||||
import org.apache.cloudstack.api.command.admin.backup.UpdateBackupOfferingCmd;
|
||||
import org.apache.cloudstack.backup.dao.BackupOfferingDao;
|
||||
|
|
@ -33,6 +43,8 @@ import org.mockito.Spy;
|
|||
import com.cloud.exception.InvalidParameterValueException;
|
||||
import com.cloud.utils.Pair;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
public class BackupManagerTest {
|
||||
@Spy
|
||||
@InjectMocks
|
||||
|
|
@ -44,6 +56,15 @@ public class BackupManagerTest {
|
|||
@Mock
|
||||
BackupProvider backupProvider;
|
||||
|
||||
@Mock
|
||||
VirtualMachineManager virtualMachineManager;
|
||||
|
||||
@Mock
|
||||
VolumeApiService volumeApiService;
|
||||
|
||||
@Mock
|
||||
VolumeDao volumeDao;
|
||||
|
||||
private String[] hostPossibleValues = {"127.0.0.1", "hostname"};
|
||||
private String[] datastoresPossibleValues = {"e9804933-8609-4de3-bccc-6278072a496c", "datastore-name"};
|
||||
|
||||
|
|
@ -189,4 +210,50 @@ public class BackupManagerTest {
|
|||
Mockito.verify(backupProvider, times(4)).restoreBackedUpVolume(Mockito.any(), Mockito.anyString(),
|
||||
Mockito.anyString(), Mockito.anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void tryRestoreVMTestRestoreSucceeded() throws NoTransitionException {
|
||||
BackupOffering offering = Mockito.mock(BackupOffering.class);
|
||||
VolumeVO volumeVO = Mockito.mock(VolumeVO.class);
|
||||
VMInstanceVO vm = Mockito.mock(VMInstanceVO.class);
|
||||
BackupVO backup = Mockito.mock(BackupVO.class);
|
||||
|
||||
Mockito.when(volumeDao.findIncludingRemovedByInstanceAndType(1L, null)).thenReturn(Collections.singletonList(volumeVO));
|
||||
Mockito.when(virtualMachineManager.stateTransitTo(Mockito.eq(vm), Mockito.eq(VirtualMachine.Event.RestoringRequested), Mockito.any())).thenReturn(true);
|
||||
Mockito.when(volumeApiService.stateTransitTo(Mockito.eq(volumeVO), Mockito.eq(Volume.Event.RestoreRequested))).thenReturn(true);
|
||||
|
||||
|
||||
|
||||
Mockito.when(vm.getId()).thenReturn(1L);
|
||||
Mockito.when(offering.getProvider()).thenReturn("veeam");
|
||||
Mockito.doReturn(backupProvider).when(backupManager).getBackupProvider("veeam");
|
||||
Mockito.when(backupProvider.restoreVMFromBackup(vm, backup)).thenReturn(true);
|
||||
|
||||
backupManager.tryRestoreVM(backup, vm, offering, "Nothing to write here.");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void tryRestoreVMTestRestoreFails() throws NoTransitionException {
|
||||
BackupOffering offering = Mockito.mock(BackupOffering.class);
|
||||
VolumeVO volumeVO = Mockito.mock(VolumeVO.class);
|
||||
VMInstanceVO vm = Mockito.mock(VMInstanceVO.class);
|
||||
BackupVO backup = Mockito.mock(BackupVO.class);
|
||||
|
||||
Mockito.when(volumeDao.findIncludingRemovedByInstanceAndType(1L, null)).thenReturn(Collections.singletonList(volumeVO));
|
||||
Mockito.when(virtualMachineManager.stateTransitTo(Mockito.eq(vm), Mockito.eq(VirtualMachine.Event.RestoringRequested), Mockito.any())).thenReturn(true);
|
||||
Mockito.when(volumeApiService.stateTransitTo(Mockito.eq(volumeVO), Mockito.eq(Volume.Event.RestoreRequested))).thenReturn(true);
|
||||
Mockito.when(virtualMachineManager.stateTransitTo(Mockito.eq(vm), Mockito.eq(VirtualMachine.Event.RestoringFailed), Mockito.any())).thenReturn(true);
|
||||
Mockito.when(volumeApiService.stateTransitTo(Mockito.eq(volumeVO), Mockito.eq(Volume.Event.RestoreFailed))).thenReturn(true);
|
||||
|
||||
Mockito.when(vm.getId()).thenReturn(1L);
|
||||
Mockito.when(offering.getProvider()).thenReturn("veeam");
|
||||
Mockito.doReturn(backupProvider).when(backupManager).getBackupProvider("veeam");
|
||||
Mockito.when(backupProvider.restoreVMFromBackup(vm, backup)).thenReturn(false);
|
||||
try {
|
||||
backupManager.tryRestoreVM(backup, vm, offering, "Checking message error.");
|
||||
fail("An exception is needed.");
|
||||
} catch (CloudRuntimeException e) {
|
||||
assertEquals("Error restoring VM from backup [Checking message error.].", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue