Skip to content

Commit 1e25340

Browse files
SadiJrSadiJr
andauthored
[Veeam] Block operations in restoring VMs (#7238)
Co-authored-by: SadiJr <sadi@scclouds.com.br>
1 parent 57ff125 commit 1e25340

File tree

7 files changed

+195
-14
lines changed

7 files changed

+195
-14
lines changed

api/src/main/java/com/cloud/storage/Volume.java

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,8 @@ enum State {
5757
UploadInProgress("Volume upload is in progress"),
5858
UploadError("Volume upload encountered some error"),
5959
UploadAbandoned("Volume upload is abandoned since the upload was never initiated within a specified time"),
60-
Attaching("The volume is attaching to a VM from Ready state.");
60+
Attaching("The volume is attaching to a VM from Ready state."),
61+
Restoring("The volume is being restored from backup.");
6162

6263
String _description;
6364

@@ -133,6 +134,11 @@ public String getDescription() {
133134
s_fsm.addTransition(new StateMachine2.Transition<State, Event>(Attaching, Event.OperationSucceeded, Ready, null));
134135
s_fsm.addTransition(new StateMachine2.Transition<State, Event>(Attaching, Event.OperationFailed, Ready, null));
135136
s_fsm.addTransition(new StateMachine2.Transition<State, Event>(Destroy, Event.RecoverRequested, Ready, null));
137+
s_fsm.addTransition(new StateMachine2.Transition<State, Event>(Ready, Event.RestoreRequested, Restoring, null));
138+
s_fsm.addTransition(new StateMachine2.Transition<State, Event>(Expunged, Event.RestoreRequested, Restoring, null));
139+
s_fsm.addTransition(new StateMachine2.Transition<State, Event>(Destroy, Event.RestoreRequested, Restoring, null));
140+
s_fsm.addTransition(new StateMachine2.Transition<State, Event>(Restoring, Event.RestoreSucceeded, Ready, null));
141+
s_fsm.addTransition(new StateMachine2.Transition<State, Event>(Restoring, Event.RestoreFailed, Ready, null));
136142
}
137143
}
138144

@@ -156,7 +162,10 @@ enum Event {
156162
ExpungingRequested,
157163
ResizeRequested,
158164
AttachRequested,
159-
OperationTimeout;
165+
OperationTimeout,
166+
RestoreRequested,
167+
RestoreSucceeded,
168+
RestoreFailed;
160169
}
161170

162171
/**

api/src/main/java/com/cloud/storage/VolumeApiService.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.net.MalformedURLException;
2222
import java.util.Map;
2323

24+
import com.cloud.utils.fsm.NoTransitionException;
2425
import org.apache.cloudstack.api.command.user.volume.AssignVolumeCmd;
2526
import org.apache.cloudstack.api.command.user.volume.AttachVolumeCmd;
2627
import org.apache.cloudstack.api.command.user.volume.ChangeOfferingForVolumeCmd;
@@ -172,4 +173,6 @@ Snapshot takeSnapshot(Long volumeId, Long policyId, Long snapshotId, Account acc
172173
Volume changeDiskOfferingForVolume(ChangeOfferingForVolumeCmd cmd) throws ResourceAllocationException;
173174

174175
void publishVolumeCreationUsageEvent(Volume volume);
176+
177+
boolean stateTransitTo(Volume vol, Volume.Event event) throws NoTransitionException;
175178
}

api/src/main/java/com/cloud/vm/VirtualMachine.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,8 @@ public enum State {
5757
Migrating(true, "VM is being migrated. host id holds to from host"),
5858
Error(false, "VM is in error"),
5959
Unknown(false, "VM state is unknown."),
60-
Shutdown(false, "VM state is shutdown from inside");
60+
Shutdown(false, "VM state is shutdown from inside"),
61+
Restoring(true, "VM is being restored from backup");
6162

6263
private final boolean _transitional;
6364
String _description;
@@ -126,6 +127,11 @@ public static StateMachine2<State, VirtualMachine.Event, VirtualMachine> getStat
126127
s_fsm.addTransition(new Transition<State, Event>(State.Expunging, VirtualMachine.Event.ExpungeOperation, State.Expunging,null));
127128
s_fsm.addTransition(new Transition<State, Event>(State.Error, VirtualMachine.Event.DestroyRequested, State.Expunging, null));
128129
s_fsm.addTransition(new Transition<State, Event>(State.Error, VirtualMachine.Event.ExpungeOperation, State.Expunging, null));
130+
s_fsm.addTransition(new Transition<State, Event>(State.Stopped, Event.RestoringRequested, State.Restoring, null));
131+
s_fsm.addTransition(new Transition<State, Event>(State.Expunging, Event.RestoringRequested, State.Restoring, null));
132+
s_fsm.addTransition(new Transition<State, Event>(State.Destroyed, Event.RestoringRequested, State.Restoring, null));
133+
s_fsm.addTransition(new Transition<State, Event>(State.Restoring, Event.RestoringSuccess, State.Stopped, null));
134+
s_fsm.addTransition(new Transition<State, Event>(State.Restoring, Event.RestoringFailed, State.Stopped, null));
129135

130136
s_fsm.addTransition(new Transition<State, Event>(State.Starting, VirtualMachine.Event.FollowAgentPowerOnReport, State.Running, Arrays.asList(new Impact[]{Impact.USAGE})));
131137
s_fsm.addTransition(new Transition<State, Event>(State.Stopping, VirtualMachine.Event.FollowAgentPowerOnReport, State.Running, null));
@@ -210,6 +216,9 @@ public enum Event {
210216
AgentReportMigrated,
211217
RevertRequested,
212218
SnapshotRequested,
219+
RestoringRequested,
220+
RestoringFailed,
221+
RestoringSuccess,
213222

214223
// added for new VMSync logic
215224
FollowAgentPowerOnReport,

server/src/main/java/com/cloud/capacity/CapacityManagerImpl.java

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -978,16 +978,12 @@ public boolean postStateTransitionEvent(StateMachine2.Transition<State, Event> t
978978
allocateVmCapacity(vm, fromLastHost);
979979
}
980980

981-
if (newState == State.Stopped) {
982-
if (vm.getType() == VirtualMachine.Type.User) {
983-
981+
if (newState == State.Stopped && event != Event.RestoringFailed && event != Event.RestoringSuccess && vm.getType() == VirtualMachine.Type.User) {
984982
UserVmVO userVM = _userVMDao.findById(vm.getId());
985983
_userVMDao.loadDetails(userVM);
986984
// free the message sent flag if it exists
987985
userVM.setDetail(VmDetailConstants.MESSAGE_RESERVED_CAPACITY_FREED_FLAG, "false");
988986
_userVMDao.saveDetails(userVM);
989-
990-
}
991987
}
992988

993989
return true;

server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1655,7 +1655,7 @@ private void removeVolume(long volumeId) {
16551655
}
16561656
}
16571657

1658-
protected boolean stateTransitTo(Volume vol, Volume.Event event) throws NoTransitionException {
1658+
public boolean stateTransitTo(Volume vol, Volume.Event event) throws NoTransitionException {
16591659
return _volStateMachine.transitTo(vol, event, null, _volsDao);
16601660
}
16611661

server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java

Lines changed: 102 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@
2727
import java.util.Timer;
2828
import java.util.TimerTask;
2929

30+
import com.cloud.storage.VolumeApiService;
31+
import com.cloud.utils.fsm.NoTransitionException;
32+
import com.cloud.vm.VirtualMachineManager;
3033
import javax.inject.Inject;
3134
import javax.naming.ConfigurationException;
3235

@@ -53,6 +56,7 @@
5356
import org.apache.cloudstack.backup.dao.BackupOfferingDao;
5457
import org.apache.cloudstack.backup.dao.BackupScheduleDao;
5558
import org.apache.cloudstack.context.CallContext;
59+
import org.apache.cloudstack.engine.orchestration.service.VolumeOrchestrationService;
5660
import org.apache.cloudstack.framework.config.ConfigKey;
5761
import org.apache.cloudstack.framework.jobs.AsyncJobDispatcher;
5862
import org.apache.cloudstack.framework.jobs.AsyncJobManager;
@@ -147,6 +151,12 @@ public class BackupManagerImpl extends ManagerBase implements BackupManager {
147151
private ApiDispatcher apiDispatcher;
148152
@Inject
149153
private AsyncJobManager asyncJobManager;
154+
@Inject
155+
private VirtualMachineManager virtualMachineManager;
156+
@Inject
157+
private VolumeApiService volumeApiService;
158+
@Inject
159+
private VolumeOrchestrationService volumeOrchestrationService;
150160

151161
private AsyncJobDispatcher asyncJobDispatcher;
152162
private Timer backupTimer;
@@ -585,17 +595,100 @@ public boolean restoreBackup(final Long backupId) {
585595

586596
final BackupOffering offering = backupOfferingDao.findByIdIncludingRemoved(vm.getBackupOfferingId());
587597
if (offering == null) {
588-
throw new CloudRuntimeException("Failed to find backup offering of the VM backup");
598+
throw new CloudRuntimeException("Failed to find backup offering of the VM backup.");
589599
}
590600

591-
final BackupProvider backupProvider = getBackupProvider(offering.getProvider());
592-
if (!backupProvider.restoreVMFromBackup(vm, backup)) {
593-
throw new CloudRuntimeException("Error restoring VM from backup ID " + backup.getId());
594-
}
601+
String backupDetailsInMessage = ReflectionToStringBuilderUtils.reflectOnlySelectedFields(backup, "uuid", "externalId", "vmId", "type", "status", "date");
602+
tryRestoreVM(backup, vm, offering, backupDetailsInMessage);
603+
updateVolumeState(vm, Volume.Event.RestoreSucceeded, Volume.State.Ready);
604+
updateVmState(vm, VirtualMachine.Event.RestoringSuccess, VirtualMachine.State.Stopped);
605+
595606
return importRestoredVM(vm.getDataCenterId(), vm.getDomainId(), vm.getAccountId(), vm.getUserId(),
596607
vm.getInstanceName(), vm.getHypervisorType(), backup);
597608
}
598609

610+
/**
611+
* Tries to restore a VM from a backup. <br/>
612+
* First update the VM state to {@link VirtualMachine.Event#RestoringRequested} and its volume states to {@link Volume.Event#RestoreRequested}, <br/>
613+
* and then try to restore the backup. <br/>
614+
*
615+
* 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}.
616+
*/
617+
protected void tryRestoreVM(BackupVO backup, VMInstanceVO vm, BackupOffering offering, String backupDetailsInMessage) {
618+
try {
619+
updateVmState(vm, VirtualMachine.Event.RestoringRequested, VirtualMachine.State.Restoring);
620+
updateVolumeState(vm, Volume.Event.RestoreRequested, Volume.State.Restoring);
621+
final BackupProvider backupProvider = getBackupProvider(offering.getProvider());
622+
if (!backupProvider.restoreVMFromBackup(vm, backup)) {
623+
throw new CloudRuntimeException(String.format("Error restoring %s from backup [%s].", vm, backupDetailsInMessage));
624+
}
625+
// The restore process is executed by a backup provider outside of ACS, I am using the catch-all (Exception) to
626+
// ensure that no provider-side exception is missed. Therefore, we have a proper handling of exceptions, and rollbacks if needed.
627+
} catch (Exception e) {
628+
LOG.error(String.format("Failed to restore backup [%s] due to: [%s].", backupDetailsInMessage, e.getMessage()), e);
629+
updateVolumeState(vm, Volume.Event.RestoreFailed, Volume.State.Ready);
630+
updateVmState(vm, VirtualMachine.Event.RestoringFailed, VirtualMachine.State.Stopped);
631+
throw new CloudRuntimeException(String.format("Error restoring VM from backup [%s].", backupDetailsInMessage));
632+
}
633+
}
634+
635+
/**
636+
* Tries to update the state of given VM, given specified event
637+
* @param vm The VM to update its state
638+
* @param event The event to update the VM state
639+
* @param next The desired state, just needed to add more context to the logs
640+
*/
641+
private void updateVmState(VMInstanceVO vm, VirtualMachine.Event event, VirtualMachine.State next) {
642+
LOG.debug(String.format("Trying to update state of VM [%s] with event [%s].", vm, event));
643+
Transaction.execute(TransactionLegacy.CLOUD_DB, (TransactionCallback<VMInstanceVO>) status -> {
644+
try {
645+
if (!virtualMachineManager.stateTransitTo(vm, event, vm.getHostId())) {
646+
throw new CloudRuntimeException(String.format("Unable to change state of VM [%s] to [%s].", vm, next));
647+
}
648+
} catch (NoTransitionException e) {
649+
String errMsg = String.format("Failed to update state of VM [%s] with event [%s] due to [%s].", vm, event, e.getMessage());
650+
LOG.error(errMsg, e);
651+
throw new RuntimeException(errMsg);
652+
}
653+
return null;
654+
});
655+
}
656+
657+
/**
658+
* Tries to update all volume states of given VM, given specified event
659+
* @param vm The VM to which the volumes belong
660+
* @param event The event to update the volume states
661+
* @param next The desired state, just needed to add more context to the logs
662+
*/
663+
private void updateVolumeState(VMInstanceVO vm, Volume.Event event, Volume.State next) {
664+
Transaction.execute(TransactionLegacy.CLOUD_DB, (TransactionCallback<VolumeVO>) status -> {
665+
for (VolumeVO volume : volumeDao.findIncludingRemovedByInstanceAndType(vm.getId(), null)) {
666+
tryToUpdateStateOfSpecifiedVolume(volume, event, next);
667+
}
668+
return null;
669+
});
670+
}
671+
672+
/**
673+
* Tries to update the state of just one volume using any passed {@link Volume.Event}. Throws an {@link RuntimeException} when fails.
674+
* @param volume The volume to update it state
675+
* @param event The event to update the volume state
676+
* @param next The desired state, just needed to add more context to the logs
677+
*
678+
*/
679+
private void tryToUpdateStateOfSpecifiedVolume(VolumeVO volume, Volume.Event event, Volume.State next) {
680+
LOG.debug(String.format("Trying to update state of volume [%s] with event [%s].", volume, event));
681+
try {
682+
if (!volumeApiService.stateTransitTo(volume, event)) {
683+
throw new CloudRuntimeException(String.format("Unable to change state of volume [%s] to [%s].", volume, next));
684+
}
685+
} catch (NoTransitionException e) {
686+
String errMsg = String.format("Failed to update state of volume [%s] with event [%s] due to [%s].", volume, event, e.getMessage());
687+
LOG.error(errMsg, e);
688+
throw new RuntimeException(errMsg);
689+
}
690+
}
691+
599692
private Backup.VolumeInfo getVolumeInfo(List<Backup.VolumeInfo> backedUpVolumes, String volumeUuid) {
600693
for (Backup.VolumeInfo volInfo : backedUpVolumes) {
601694
if (volInfo.getUuid().equals(volumeUuid)) {
@@ -652,16 +745,20 @@ public boolean restoreBackupVolumeAndAttachToVM(final String backedUpVolumeUuid,
652745
String[] hostPossibleValues = {host.getPrivateIpAddress(), host.getName()};
653746
String[] datastoresPossibleValues = {datastore.getUuid(), datastore.getName()};
654747

748+
updateVmState(vm, VirtualMachine.Event.RestoringRequested, VirtualMachine.State.Restoring);
655749
Pair<Boolean, String> result = restoreBackedUpVolume(backedUpVolumeUuid, backup, backupProvider, hostPossibleValues, datastoresPossibleValues);
656750

657751
if (BooleanUtils.isFalse(result.first())) {
752+
updateVmState(vm, VirtualMachine.Event.RestoringFailed, VirtualMachine.State.Stopped);
658753
throw new CloudRuntimeException(String.format("Error restoring volume [%s] of VM [%s] to host [%s] using backup provider [%s] due to: [%s].",
659754
backedUpVolumeUuid, vm.getUuid(), host.getUuid(), backupProvider.getName(), result.second()));
660755
}
661756
if (!attachVolumeToVM(vm.getDataCenterId(), result.second(), vmFromBackup.getBackupVolumeList(),
662757
backedUpVolumeUuid, vm, datastore.getUuid(), backup)) {
758+
updateVmState(vm, VirtualMachine.Event.RestoringFailed, VirtualMachine.State.Stopped);
663759
throw new CloudRuntimeException(String.format("Error attaching volume [%s] to VM [%s]." + backedUpVolumeUuid, vm.getUuid()));
664760
}
761+
updateVmState(vm, VirtualMachine.Event.RestoringSuccess, VirtualMachine.State.Stopped);
665762
return true;
666763
}
667764

server/src/test/java/org/apache/cloudstack/backup/BackupManagerTest.java

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,19 @@
1616
// under the License.
1717
package org.apache.cloudstack.backup;
1818
import static org.junit.Assert.assertEquals;
19+
import static org.junit.Assert.fail;
1920
import static org.mockito.Mockito.times;
2021
import static org.mockito.Mockito.when;
2122

23+
import com.cloud.storage.Volume;
24+
import com.cloud.storage.VolumeApiService;
25+
import com.cloud.storage.VolumeVO;
26+
import com.cloud.storage.dao.VolumeDao;
27+
import com.cloud.utils.exception.CloudRuntimeException;
28+
import com.cloud.utils.fsm.NoTransitionException;
29+
import com.cloud.vm.VMInstanceVO;
30+
import com.cloud.vm.VirtualMachine;
31+
import com.cloud.vm.VirtualMachineManager;
2232
import org.apache.cloudstack.api.ServerApiException;
2333
import org.apache.cloudstack.api.command.admin.backup.UpdateBackupOfferingCmd;
2434
import org.apache.cloudstack.backup.dao.BackupOfferingDao;
@@ -33,6 +43,8 @@
3343
import com.cloud.exception.InvalidParameterValueException;
3444
import com.cloud.utils.Pair;
3545

46+
import java.util.Collections;
47+
3648
public class BackupManagerTest {
3749
@Spy
3850
@InjectMocks
@@ -44,6 +56,15 @@ public class BackupManagerTest {
4456
@Mock
4557
BackupProvider backupProvider;
4658

59+
@Mock
60+
VirtualMachineManager virtualMachineManager;
61+
62+
@Mock
63+
VolumeApiService volumeApiService;
64+
65+
@Mock
66+
VolumeDao volumeDao;
67+
4768
private String[] hostPossibleValues = {"127.0.0.1", "hostname"};
4869
private String[] datastoresPossibleValues = {"e9804933-8609-4de3-bccc-6278072a496c", "datastore-name"};
4970

@@ -189,4 +210,50 @@ public void restoreBackedUpVolumeTestHostAndDatastoreName() {
189210
Mockito.verify(backupProvider, times(4)).restoreBackedUpVolume(Mockito.any(), Mockito.anyString(),
190211
Mockito.anyString(), Mockito.anyString());
191212
}
213+
214+
@Test
215+
public void tryRestoreVMTestRestoreSucceeded() throws NoTransitionException {
216+
BackupOffering offering = Mockito.mock(BackupOffering.class);
217+
VolumeVO volumeVO = Mockito.mock(VolumeVO.class);
218+
VMInstanceVO vm = Mockito.mock(VMInstanceVO.class);
219+
BackupVO backup = Mockito.mock(BackupVO.class);
220+
221+
Mockito.when(volumeDao.findIncludingRemovedByInstanceAndType(1L, null)).thenReturn(Collections.singletonList(volumeVO));
222+
Mockito.when(virtualMachineManager.stateTransitTo(Mockito.eq(vm), Mockito.eq(VirtualMachine.Event.RestoringRequested), Mockito.any())).thenReturn(true);
223+
Mockito.when(volumeApiService.stateTransitTo(Mockito.eq(volumeVO), Mockito.eq(Volume.Event.RestoreRequested))).thenReturn(true);
224+
225+
226+
227+
Mockito.when(vm.getId()).thenReturn(1L);
228+
Mockito.when(offering.getProvider()).thenReturn("veeam");
229+
Mockito.doReturn(backupProvider).when(backupManager).getBackupProvider("veeam");
230+
Mockito.when(backupProvider.restoreVMFromBackup(vm, backup)).thenReturn(true);
231+
232+
backupManager.tryRestoreVM(backup, vm, offering, "Nothing to write here.");
233+
}
234+
235+
@Test
236+
public void tryRestoreVMTestRestoreFails() throws NoTransitionException {
237+
BackupOffering offering = Mockito.mock(BackupOffering.class);
238+
VolumeVO volumeVO = Mockito.mock(VolumeVO.class);
239+
VMInstanceVO vm = Mockito.mock(VMInstanceVO.class);
240+
BackupVO backup = Mockito.mock(BackupVO.class);
241+
242+
Mockito.when(volumeDao.findIncludingRemovedByInstanceAndType(1L, null)).thenReturn(Collections.singletonList(volumeVO));
243+
Mockito.when(virtualMachineManager.stateTransitTo(Mockito.eq(vm), Mockito.eq(VirtualMachine.Event.RestoringRequested), Mockito.any())).thenReturn(true);
244+
Mockito.when(volumeApiService.stateTransitTo(Mockito.eq(volumeVO), Mockito.eq(Volume.Event.RestoreRequested))).thenReturn(true);
245+
Mockito.when(virtualMachineManager.stateTransitTo(Mockito.eq(vm), Mockito.eq(VirtualMachine.Event.RestoringFailed), Mockito.any())).thenReturn(true);
246+
Mockito.when(volumeApiService.stateTransitTo(Mockito.eq(volumeVO), Mockito.eq(Volume.Event.RestoreFailed))).thenReturn(true);
247+
248+
Mockito.when(vm.getId()).thenReturn(1L);
249+
Mockito.when(offering.getProvider()).thenReturn("veeam");
250+
Mockito.doReturn(backupProvider).when(backupManager).getBackupProvider("veeam");
251+
Mockito.when(backupProvider.restoreVMFromBackup(vm, backup)).thenReturn(false);
252+
try {
253+
backupManager.tryRestoreVM(backup, vm, offering, "Checking message error.");
254+
fail("An exception is needed.");
255+
} catch (CloudRuntimeException e) {
256+
assertEquals("Error restoring VM from backup [Checking message error.].", e.getMessage());
257+
}
258+
}
192259
}

0 commit comments

Comments
 (0)