|
27 | 27 | import java.util.Timer; |
28 | 28 | import java.util.TimerTask; |
29 | 29 |
|
| 30 | +import com.cloud.storage.VolumeApiService; |
| 31 | +import com.cloud.utils.fsm.NoTransitionException; |
| 32 | +import com.cloud.vm.VirtualMachineManager; |
30 | 33 | import javax.inject.Inject; |
31 | 34 | import javax.naming.ConfigurationException; |
32 | 35 |
|
|
53 | 56 | import org.apache.cloudstack.backup.dao.BackupOfferingDao; |
54 | 57 | import org.apache.cloudstack.backup.dao.BackupScheduleDao; |
55 | 58 | import org.apache.cloudstack.context.CallContext; |
| 59 | +import org.apache.cloudstack.engine.orchestration.service.VolumeOrchestrationService; |
56 | 60 | import org.apache.cloudstack.framework.config.ConfigKey; |
57 | 61 | import org.apache.cloudstack.framework.jobs.AsyncJobDispatcher; |
58 | 62 | import org.apache.cloudstack.framework.jobs.AsyncJobManager; |
@@ -147,6 +151,12 @@ public class BackupManagerImpl extends ManagerBase implements BackupManager { |
147 | 151 | private ApiDispatcher apiDispatcher; |
148 | 152 | @Inject |
149 | 153 | private AsyncJobManager asyncJobManager; |
| 154 | + @Inject |
| 155 | + private VirtualMachineManager virtualMachineManager; |
| 156 | + @Inject |
| 157 | + private VolumeApiService volumeApiService; |
| 158 | + @Inject |
| 159 | + private VolumeOrchestrationService volumeOrchestrationService; |
150 | 160 |
|
151 | 161 | private AsyncJobDispatcher asyncJobDispatcher; |
152 | 162 | private Timer backupTimer; |
@@ -585,17 +595,100 @@ public boolean restoreBackup(final Long backupId) { |
585 | 595 |
|
586 | 596 | final BackupOffering offering = backupOfferingDao.findByIdIncludingRemoved(vm.getBackupOfferingId()); |
587 | 597 | 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."); |
589 | 599 | } |
590 | 600 |
|
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 | + |
595 | 606 | return importRestoredVM(vm.getDataCenterId(), vm.getDomainId(), vm.getAccountId(), vm.getUserId(), |
596 | 607 | vm.getInstanceName(), vm.getHypervisorType(), backup); |
597 | 608 | } |
598 | 609 |
|
| 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 | + |
599 | 692 | private Backup.VolumeInfo getVolumeInfo(List<Backup.VolumeInfo> backedUpVolumes, String volumeUuid) { |
600 | 693 | for (Backup.VolumeInfo volInfo : backedUpVolumes) { |
601 | 694 | if (volInfo.getUuid().equals(volumeUuid)) { |
@@ -652,16 +745,20 @@ public boolean restoreBackupVolumeAndAttachToVM(final String backedUpVolumeUuid, |
652 | 745 | String[] hostPossibleValues = {host.getPrivateIpAddress(), host.getName()}; |
653 | 746 | String[] datastoresPossibleValues = {datastore.getUuid(), datastore.getName()}; |
654 | 747 |
|
| 748 | + updateVmState(vm, VirtualMachine.Event.RestoringRequested, VirtualMachine.State.Restoring); |
655 | 749 | Pair<Boolean, String> result = restoreBackedUpVolume(backedUpVolumeUuid, backup, backupProvider, hostPossibleValues, datastoresPossibleValues); |
656 | 750 |
|
657 | 751 | if (BooleanUtils.isFalse(result.first())) { |
| 752 | + updateVmState(vm, VirtualMachine.Event.RestoringFailed, VirtualMachine.State.Stopped); |
658 | 753 | throw new CloudRuntimeException(String.format("Error restoring volume [%s] of VM [%s] to host [%s] using backup provider [%s] due to: [%s].", |
659 | 754 | backedUpVolumeUuid, vm.getUuid(), host.getUuid(), backupProvider.getName(), result.second())); |
660 | 755 | } |
661 | 756 | if (!attachVolumeToVM(vm.getDataCenterId(), result.second(), vmFromBackup.getBackupVolumeList(), |
662 | 757 | backedUpVolumeUuid, vm, datastore.getUuid(), backup)) { |
| 758 | + updateVmState(vm, VirtualMachine.Event.RestoringFailed, VirtualMachine.State.Stopped); |
663 | 759 | throw new CloudRuntimeException(String.format("Error attaching volume [%s] to VM [%s]." + backedUpVolumeUuid, vm.getUuid())); |
664 | 760 | } |
| 761 | + updateVmState(vm, VirtualMachine.Event.RestoringSuccess, VirtualMachine.State.Stopped); |
665 | 762 | return true; |
666 | 763 | } |
667 | 764 |
|
|
0 commit comments