diff --git a/api/src/com/cloud/vm/snapshot/VMSnapshot.java b/api/src/com/cloud/vm/snapshot/VMSnapshot.java index 7713b2052ace..c398e583fae5 100644 --- a/api/src/com/cloud/vm/snapshot/VMSnapshot.java +++ b/api/src/com/cloud/vm/snapshot/VMSnapshot.java @@ -102,4 +102,6 @@ enum Event { @Override public long getAccountId(); + + public long getServiceOfferingId(); } diff --git a/engine/schema/src/com/cloud/vm/snapshot/VMSnapshotVO.java b/engine/schema/src/com/cloud/vm/snapshot/VMSnapshotVO.java index df2966d45b66..c48396ad0219 100644 --- a/engine/schema/src/com/cloud/vm/snapshot/VMSnapshotVO.java +++ b/engine/schema/src/com/cloud/vm/snapshot/VMSnapshotVO.java @@ -72,6 +72,9 @@ public class VMSnapshotVO implements VMSnapshot { @Column(name = "domain_id") long domainId; + @Column(name = "service_offering_id") + private long serviceOfferingId; + @Column(name = "vm_snapshot_type") @Enumerated(EnumType.STRING) VMSnapshot.Type type; @@ -139,6 +142,7 @@ public VMSnapshotVO(Long accountId, Long domainId, Long vmId, String description displayName = vsDisplayName; this.type = type; this.current = current; + this.serviceOfferingId = serviceOfferingId; } @Override @@ -248,4 +252,9 @@ public void setRemoved(Date removed) { public Class getEntityType() { return VMSnapshot.class; } + + @Override + public long getServiceOfferingId() { + return serviceOfferingId; + } } diff --git a/server/src/com/cloud/vm/UserVmManagerImpl.java b/server/src/com/cloud/vm/UserVmManagerImpl.java index 3f21a92a21eb..f7449e78c8d5 100755 --- a/server/src/com/cloud/vm/UserVmManagerImpl.java +++ b/server/src/com/cloud/vm/UserVmManagerImpl.java @@ -950,12 +950,6 @@ public UserVm upgradeVirtualMachine(UpgradeVMCmd cmd) throws ResourceAllocationE + "; make sure the virtual machine is stopped"); } - // If target VM has associated VM snapshots then don't allow upgrading of VM - List vmSnapshots = _vmSnapshotDao.findByVm(vmId); - if (vmSnapshots.size() > 0) { - throw new InvalidParameterValueException("Unable to change service offering for VM, please remove VM snapshots before changing service offering of VM"); - } - _accountMgr.checkAccess(caller, null, true, vmInstance); // Check resource limits for CPU and Memory. @@ -1616,11 +1610,6 @@ public boolean upgradeVirtualMachine(Long vmId, Long newServiceOfferingId, Map vmSnapshots = _vmSnapshotDao.findByVm(vmId); - if (vmSnapshots.size() > 0) { - throw new InvalidParameterValueException("Unable to scale VM, please remove VM snapshots before scaling VM"); - } if (vmInstance.getState().equals(State.Stopped)) { upgradeStoppedVirtualMachine(vmId, newServiceOfferingId, customParameters); return true; diff --git a/server/src/com/cloud/vm/snapshot/VMSnapshotManagerImpl.java b/server/src/com/cloud/vm/snapshot/VMSnapshotManagerImpl.java index 5f9115f2b7da..2a16aaaadd07 100644 --- a/server/src/com/cloud/vm/snapshot/VMSnapshotManagerImpl.java +++ b/server/src/com/cloud/vm/snapshot/VMSnapshotManagerImpl.java @@ -57,12 +57,16 @@ import com.cloud.exception.ConcurrentOperationException; import com.cloud.exception.InsufficientCapacityException; import com.cloud.exception.InvalidParameterValueException; +import com.cloud.exception.ManagementServerException; import com.cloud.exception.ResourceAllocationException; import com.cloud.exception.ResourceUnavailableException; +import com.cloud.exception.VirtualMachineMigrationException; import com.cloud.gpu.GPU; import com.cloud.hypervisor.Hypervisor.HypervisorType; import com.cloud.hypervisor.dao.HypervisorCapabilitiesDao; import com.cloud.projects.Project.ListProjectResourcesCriteria; +import com.cloud.service.ServiceOfferingVO; +import com.cloud.service.dao.ServiceOfferingDao; import com.cloud.service.dao.ServiceOfferingDetailsDao; import com.cloud.storage.GuestOSVO; import com.cloud.storage.Snapshot; @@ -89,7 +93,13 @@ import com.cloud.utils.db.Filter; import com.cloud.utils.db.SearchBuilder; import com.cloud.utils.db.SearchCriteria; +import com.cloud.utils.db.Transaction; +import com.cloud.utils.db.TransactionCallbackWithException; +import com.cloud.utils.db.TransactionCallbackWithExceptionNoReturn; +import com.cloud.utils.db.TransactionStatus; import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.vm.UserVmDetailVO; +import com.cloud.vm.UserVmManager; import com.cloud.vm.UserVmVO; import com.cloud.vm.VMInstanceVO; import com.cloud.vm.VirtualMachine; @@ -102,8 +112,10 @@ import com.cloud.vm.VmWorkJobHandlerProxy; import com.cloud.vm.VmWorkSerializer; import com.cloud.vm.dao.UserVmDao; +import com.cloud.vm.dao.UserVmDetailsDao; import com.cloud.vm.dao.VMInstanceDao; import com.cloud.vm.snapshot.dao.VMSnapshotDao; +import com.cloud.vm.snapshot.dao.VMSnapshotDetailsDao; @Component public class VMSnapshotManagerImpl extends MutualExclusiveIdsManagerBase implements VMSnapshotManager, VMSnapshotService, VmWorkJobHandler { @@ -135,6 +147,14 @@ public class VMSnapshotManagerImpl extends MutualExclusiveIdsManagerBase impleme @Inject VmWorkJobDao _workJobDao; + @Inject + protected UserVmManager _userVmManager; + @Inject + protected ServiceOfferingDao _serviceOfferingDao; + @Inject + protected UserVmDetailsDao _userVmDetailsDao; + @Inject + protected VMSnapshotDetailsDao _vmSnapshotDetailsDao; VmWorkJobHandlerProxy _jobHandlerProxy = new VmWorkJobHandlerProxy(this); @@ -346,14 +366,7 @@ public VMSnapshot allocVMSnapshot(Long vmId, String vsDisplayName, String vsDesc vmSnapshotType = VMSnapshot.Type.DiskAndMemory; try { - VMSnapshotVO vmSnapshotVo = - new VMSnapshotVO(userVmVo.getAccountId(), userVmVo.getDomainId(), vmId, vsDescription, vmSnapshotName, vsDisplayName, userVmVo.getServiceOfferingId(), - vmSnapshotType, null); - VMSnapshot vmSnapshot = _vmSnapshotDao.persist(vmSnapshotVo); - if (vmSnapshot == null) { - throw new CloudRuntimeException("Failed to create snapshot for vm: " + vmId); - } - return vmSnapshot; + return createAndPersistVMSnapshot(userVmVo, vsDescription, vmSnapshotName, vsDisplayName, vmSnapshotType); } catch (Exception e) { String msg = e.getMessage(); s_logger.error("Create vm snapshot record failed for vm: " + vmId + " due to: " + msg); @@ -361,6 +374,57 @@ public VMSnapshot allocVMSnapshot(Long vmId, String vsDisplayName, String vsDesc return null; } + /** + * Create, persist and return vm snapshot for userVmVo with given parameters. + * Persistence and support for custom service offerings are done on the same transaction + * @param userVmVo user vm + * @param vmId vm id + * @param vsDescription vm description + * @param vmSnapshotName vm snapshot name + * @param vsDisplayName vm snapshot display name + * @param vmSnapshotType vm snapshot type + * @return vm snapshot + * @throws CloudRuntimeException if vm snapshot couldn't be persisted + */ + protected VMSnapshot createAndPersistVMSnapshot(UserVmVO userVmVo, String vsDescription, String vmSnapshotName, String vsDisplayName, VMSnapshot.Type vmSnapshotType) { + final Long vmId = userVmVo.getId(); + final Long serviceOfferingId = userVmVo.getServiceOfferingId(); + final VMSnapshotVO vmSnapshotVo = + new VMSnapshotVO(userVmVo.getAccountId(), userVmVo.getDomainId(), vmId, vsDescription, vmSnapshotName, vsDisplayName, serviceOfferingId, + vmSnapshotType, null); + return Transaction.execute(new TransactionCallbackWithException() { + @Override + public VMSnapshot doInTransaction(TransactionStatus status) { + VMSnapshot vmSnapshot = _vmSnapshotDao.persist(vmSnapshotVo); + if (vmSnapshot == null) { + throw new CloudRuntimeException("Failed to create snapshot for vm: " + vmId); + } + addSupportForCustomServiceOffering(vmId, serviceOfferingId, vmSnapshot.getId()); + return vmSnapshot; + } + }); + } + + /** + * Add entries on vm_snapshot_details if service offering is dynamic. This will allow setting details when revert to vm snapshot + * @param vmId vm id + * @param serviceOfferingId service offering id + * @param vmSnapshotId vm snapshot id + */ + protected void addSupportForCustomServiceOffering(long vmId, long serviceOfferingId, long vmSnapshotId) { + ServiceOfferingVO serviceOfferingVO = _serviceOfferingDao.findById(serviceOfferingId); + if (serviceOfferingVO.isDynamic()) { + List vmDetails = _userVmDetailsDao.listDetails(vmId); + List vmSnapshotDetails = new ArrayList(); + for (UserVmDetailVO detail : vmDetails) { + if(detail.getName().equalsIgnoreCase("cpuNumber") || detail.getName().equalsIgnoreCase("cpuSpeed") || detail.getName().equalsIgnoreCase("memory")) { + vmSnapshotDetails.add(new VMSnapshotDetailsVO(vmSnapshotId, detail.getName(), detail.getValue(), detail.isDisplay())); + } + } + _vmSnapshotDetailsDao.saveDetails(vmSnapshotDetails); + } + } + @Override public String getName() { return _name; @@ -654,16 +718,76 @@ else if (jobResult instanceof Throwable) } } + /** + * If snapshot was taken with a different service offering than actual used in vm, should change it back to it + * @param userVm vm to change service offering (if necessary) + * @param vmSnapshotVo vm snapshot + */ + protected void updateUserVmServiceOffering(UserVm userVm, VMSnapshotVO vmSnapshotVo) { + if (vmSnapshotVo.getServiceOfferingId() != userVm.getServiceOfferingId()) { + changeUserVmServiceOffering(userVm, vmSnapshotVo); + } + } + + /** + * Get user vm details as a map + * @param userVm user vm + * @return map + */ + protected Map getVmMapDetails(UserVm userVm) { + List userVmDetails = _userVmDetailsDao.listDetails(userVm.getId()); + Map details = new HashMap(); + for (UserVmDetailVO detail : userVmDetails) { + details.put(detail.getName(), detail.getValue()); + } + return details; + } + + /** + * Update service offering on {@link userVm} to the one specified in {@link vmSnapshotVo} + * @param userVm user vm to be updated + * @param vmSnapshotVo vm snapshot + */ + protected void changeUserVmServiceOffering(UserVm userVm, VMSnapshotVO vmSnapshotVo) { + Map vmDetails = getVmMapDetails(userVm); + boolean result = upgradeUserVmServiceOffering(userVm.getId(), vmSnapshotVo.getServiceOfferingId(), vmDetails); + if (! result){ + throw new CloudRuntimeException("VM Snapshot reverting failed due to vm service offering couldn't be changed to the one used when snapshot was taken"); + } + s_logger.debug("Successfully changed service offering to " + vmSnapshotVo.getServiceOfferingId() + " for vm " + userVm.getId()); + } + + /** + * Upgrade virtual machine {@linkplain vmId} to new service offering {@linkplain serviceOfferingId} + * @param vmId vm id + * @param serviceOfferingId service offering id + * @param details vm details + * @return if operation was successful + */ + protected boolean upgradeUserVmServiceOffering(Long vmId, Long serviceOfferingId, Map details) { + boolean result; + try { + result = _userVmManager.upgradeVirtualMachine(vmId, serviceOfferingId, details); + if (! result){ + s_logger.error("Couldn't change service offering for vm " + vmId + " to " + serviceOfferingId); + } + return result; + } catch (ConcurrentOperationException | ResourceUnavailableException | ManagementServerException | VirtualMachineMigrationException e) { + s_logger.error("Couldn't change service offering for vm " + vmId + " to " + serviceOfferingId + " due to: " + e.getMessage()); + return false; + } + } + private UserVm orchestrateRevertToVMSnapshot(Long vmSnapshotId) throws InsufficientCapacityException, ResourceUnavailableException, ConcurrentOperationException { // check if VM snapshot exists in DB - VMSnapshotVO vmSnapshotVo = _vmSnapshotDao.findById(vmSnapshotId); + final VMSnapshotVO vmSnapshotVo = _vmSnapshotDao.findById(vmSnapshotId); if (vmSnapshotVo == null) { throw new InvalidParameterValueException( "unable to find the vm snapshot with id " + vmSnapshotId); } Long vmId = vmSnapshotVo.getVmId(); - UserVmVO userVm = _userVMDao.findById(vmId); + final UserVmVO userVm = _userVMDao.findById(vmId); // check if VM exists if (userVm == null) { throw new InvalidParameterValueException("Revert vm to snapshot: " @@ -721,6 +845,13 @@ private UserVm orchestrateRevertToVMSnapshot(Long vmSnapshotId) throws Insuffici try { VMSnapshotStrategy strategy = findVMSnapshotStrategy(vmSnapshotVo); strategy.revertVMSnapshot(vmSnapshotVo); + Transaction.execute(new TransactionCallbackWithExceptionNoReturn() { + @Override + public void doInTransactionWithoutResult(TransactionStatus status) throws CloudRuntimeException { + revertUserVmDetailsFromVmSnapshot(userVm, vmSnapshotVo); + updateUserVmServiceOffering(userVm, vmSnapshotVo); + } + }); return userVm; } catch (Exception e) { s_logger.debug("Failed to revert vmsnapshot: " + vmSnapshotId, e); @@ -728,6 +859,23 @@ private UserVm orchestrateRevertToVMSnapshot(Long vmSnapshotId) throws Insuffici } } + /** + * Update or add user vm details from vm snapshot for vms with custom service offerings + * @param userVm user vm + * @param vmSnapshotVo vm snapshot + */ + protected void revertUserVmDetailsFromVmSnapshot(UserVmVO userVm, VMSnapshotVO vmSnapshotVo) { + ServiceOfferingVO serviceOfferingVO = _serviceOfferingDao.findById(vmSnapshotVo.getServiceOfferingId()); + if (serviceOfferingVO.isDynamic()) { + List vmSnapshotDetails = _vmSnapshotDetailsDao.listDetails(vmSnapshotVo.getId()); + List userVmDetails = new ArrayList(); + for (VMSnapshotDetailsVO detail : vmSnapshotDetails) { + userVmDetails.add(new UserVmDetailVO(userVm.getId(), detail.getName(), detail.getValue(), detail.isDisplay())); + } + _userVmDetailsDao.saveDetails(userVmDetails); + } + } + @Override public RestoreVMSnapshotCommand createRestoreCommand(UserVmVO userVm, List vmSnapshotVOs) { if (!HypervisorType.KVM.equals(userVm.getHypervisorType())) diff --git a/server/test/com/cloud/vm/snapshot/VMSnapshotManagerTest.java b/server/test/com/cloud/vm/snapshot/VMSnapshotManagerTest.java index 9d1ed4f6a748..2cf763fe93c8 100644 --- a/server/test/com/cloud/vm/snapshot/VMSnapshotManagerTest.java +++ b/server/test/com/cloud/vm/snapshot/VMSnapshotManagerTest.java @@ -16,6 +16,7 @@ // under the License. package com.cloud.vm.snapshot; +import static org.junit.Assert.assertEquals; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyLong; import static org.mockito.Matchers.anyString; @@ -23,30 +24,45 @@ import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.never; import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; import org.junit.Before; import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Matchers; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.Spy; import org.apache.cloudstack.acl.ControlledEntity; import org.apache.cloudstack.acl.SecurityChecker.AccessType; +import org.apache.cloudstack.api.ResourceDetail; import org.apache.cloudstack.framework.config.dao.ConfigurationDao; import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; import com.cloud.agent.AgentManager; import com.cloud.exception.AgentUnavailableException; +import com.cloud.exception.ConcurrentOperationException; import com.cloud.exception.InvalidParameterValueException; +import com.cloud.exception.ManagementServerException; import com.cloud.exception.OperationTimedoutException; import com.cloud.exception.ResourceAllocationException; +import com.cloud.exception.ResourceUnavailableException; +import com.cloud.exception.VirtualMachineMigrationException; import com.cloud.host.dao.HostDao; import com.cloud.hypervisor.Hypervisor; import com.cloud.hypervisor.HypervisorGuruManager; import com.cloud.hypervisor.dao.HypervisorCapabilitiesDao; +import com.cloud.service.ServiceOfferingVO; +import com.cloud.service.dao.ServiceOfferingDao; import com.cloud.service.dao.ServiceOfferingDetailsDao; import com.cloud.storage.GuestOSVO; import com.cloud.storage.Snapshot; @@ -59,14 +75,19 @@ import com.cloud.user.AccountManager; import com.cloud.user.dao.AccountDao; import com.cloud.user.dao.UserDao; +import com.cloud.uservm.UserVm; import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.utils.fsm.NoTransitionException; +import com.cloud.vm.UserVmDetailVO; +import com.cloud.vm.UserVmManager; import com.cloud.vm.UserVmVO; import com.cloud.vm.VirtualMachine.State; import com.cloud.vm.VirtualMachineManager; import com.cloud.vm.dao.UserVmDao; +import com.cloud.vm.dao.UserVmDetailsDao; import com.cloud.vm.dao.VMInstanceDao; import com.cloud.vm.snapshot.dao.VMSnapshotDao; +import com.cloud.vm.snapshot.dao.VMSnapshotDetailsDao; public class VMSnapshotManagerTest { @Spy @@ -107,13 +128,52 @@ public class VMSnapshotManagerTest { HypervisorCapabilitiesDao _hypervisorCapabilitiesDao; @Mock ServiceOfferingDetailsDao _serviceOfferingDetailsDao; + @Mock + ServiceOfferingDao _serviceOfferingDao; + @Mock + UserVmDetailsDao _userVmDetailsDao; + @Mock + VMSnapshotDetailsDao _vmSnapshotDetailsDao; + @Mock + UserVmManager _userVmManager; int _vmSnapshotMax = 10; private static final long TEST_VM_ID = 3L; + private static final long SERVICE_OFFERING_ID = 1L; + private static final long SERVICE_OFFERING_DIFFERENT_ID = 2L; + private static VMSnapshot.Type vmSnapshotType; + private static List userVmDetails; + private static List vmSnapshotDetails; + + private static final long VM_SNAPSHOT_ID = 1L; + private static final String VM_SNAPSHOT_NAME = "Vm-Snapshot-Name"; + private static final String VM_SNAPSHOT_DESCRIPTION = "Vm-Snapshot-Desc"; + private static final String VM_SNAPSHOT_DISPLAY_NAME = "Vm-Snapshot-Display-Name"; @Mock UserVmVO vmMock; @Mock VolumeVO volumeMock; + @Mock + VMSnapshotVO vmSnapshotVO; + @Mock + ServiceOfferingVO serviceOffering; + @Mock + UserVmDetailVO userVmDetailCpuNumber; + @Mock + UserVmDetailVO userVmDetailMemory; + @Mock + VMSnapshotDetailsVO vmSnapshotDetailCpuNumber; + @Mock + VMSnapshotDetailsVO vmSnapshotDetailMemory; + @Mock + UserVm userVm; + + @Captor + ArgumentCaptor> listVmSnapshotDetailsCaptor; + @Captor + ArgumentCaptor> mapDetailsCaptor; + @Captor + ArgumentCaptor> listUserVmDetailsCaptor; @Before public void setup() { @@ -133,6 +193,11 @@ public void setup() { _vmSnapshotMgr._vmSnapshotMax = _vmSnapshotMax; + _vmSnapshotMgr._serviceOfferingDao = _serviceOfferingDao; + _vmSnapshotMgr._userVmDetailsDao = _userVmDetailsDao; + _vmSnapshotMgr._vmSnapshotDetailsDao = _vmSnapshotDetailsDao; + _vmSnapshotMgr._userVmManager = _userVmManager; + when(_userVMDao.findById(anyLong())).thenReturn(vmMock); when(_vmSnapshotDao.findByName(anyLong(), anyString())).thenReturn(null); when(_vmSnapshotDao.findByVm(anyLong())).thenReturn(new ArrayList()); @@ -144,10 +209,40 @@ public void setup() { when(volumeMock.getInstanceId()).thenReturn(TEST_VM_ID); when(_volumeDao.findByInstance(anyLong())).thenReturn(mockVolumeList); + when(vmMock.getId()).thenReturn(TEST_VM_ID); + when(vmMock.getServiceOfferingId()).thenReturn(SERVICE_OFFERING_ID); + when(vmMock.getAccountId()).thenReturn(1L); + when(vmMock.getDomainId()).thenReturn(1L); when(vmMock.getInstanceName()).thenReturn("i-3-VM-TEST"); when(vmMock.getState()).thenReturn(State.Running); when(vmMock.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.XenServer); when(_guestOSDao.findById(anyLong())).thenReturn(mock(GuestOSVO.class)); + + when(vmSnapshotVO.getId()).thenReturn(VM_SNAPSHOT_ID); + when(serviceOffering.isDynamic()).thenReturn(false); + when(_serviceOfferingDao.findById(SERVICE_OFFERING_ID)).thenReturn(serviceOffering); + + for (ResourceDetail detail : Arrays.asList(userVmDetailCpuNumber, vmSnapshotDetailCpuNumber)) { + when(detail.getName()).thenReturn("cpuNumber"); + when(detail.getValue()).thenReturn("2"); + when(detail.isDisplay()).thenReturn(true); + } + + for (ResourceDetail detail : Arrays.asList(userVmDetailMemory, vmSnapshotDetailMemory)) { + when(detail.getName()).thenReturn("memory"); + when(detail.getValue()).thenReturn("2048"); + when(detail.isDisplay()).thenReturn(true); + } + + userVmDetails = Arrays.asList(userVmDetailCpuNumber, userVmDetailMemory); + vmSnapshotDetails = Arrays.asList(vmSnapshotDetailCpuNumber, vmSnapshotDetailMemory); + when(_userVmDetailsDao.listDetails(TEST_VM_ID)).thenReturn(userVmDetails); + when(_vmSnapshotDetailsDao.listDetails(VM_SNAPSHOT_ID)).thenReturn(vmSnapshotDetails); + + when(userVm.getId()).thenReturn(TEST_VM_ID); + when(userVm.getServiceOfferingId()).thenReturn(SERVICE_OFFERING_ID); + + when(vmSnapshotVO.getServiceOfferingId()).thenReturn(SERVICE_OFFERING_ID); } // vmId null case @@ -207,4 +302,104 @@ public void testCreateVMSnapshot() throws AgentUnavailableException, OperationTi } + @Test + public void testCreateAndPersistVMSnapshot() { + when(_vmSnapshotDao.persist(any(VMSnapshotVO.class))).thenReturn(vmSnapshotVO); + _vmSnapshotMgr.createAndPersistVMSnapshot(vmMock, VM_SNAPSHOT_DESCRIPTION, + VM_SNAPSHOT_NAME, VM_SNAPSHOT_DISPLAY_NAME, vmSnapshotType); + + verify(_vmSnapshotMgr).addSupportForCustomServiceOffering(TEST_VM_ID, SERVICE_OFFERING_ID, VM_SNAPSHOT_ID); + } + + @Test(expected=CloudRuntimeException.class) + public void testCreateAndPersistVMSnapshotNullVMSnapshot() { + when(_vmSnapshotDao.persist(any(VMSnapshotVO.class))).thenReturn(null); + _vmSnapshotMgr.createAndPersistVMSnapshot(vmMock, VM_SNAPSHOT_DESCRIPTION, + VM_SNAPSHOT_NAME, VM_SNAPSHOT_DISPLAY_NAME, vmSnapshotType); + } + + @Test + public void testAddSupportForCustomServiceOfferingNotDynamicServiceOffering() { + _vmSnapshotMgr.addSupportForCustomServiceOffering(TEST_VM_ID, SERVICE_OFFERING_ID, VM_SNAPSHOT_ID); + verify(_userVmDetailsDao, never()).listDetails(TEST_VM_ID); + } + + @Test + public void testAddSupportForCustomServiceOfferingDynamicServiceOffering() { + when(serviceOffering.isDynamic()).thenReturn(true); + _vmSnapshotMgr.addSupportForCustomServiceOffering(TEST_VM_ID, SERVICE_OFFERING_ID, VM_SNAPSHOT_ID); + + verify(_userVmDetailsDao).listDetails(TEST_VM_ID); + verify(_vmSnapshotDetailsDao).saveDetails(listVmSnapshotDetailsCaptor.capture()); + } + + @Test + public void testUpdateUserVmServiceOfferingSameServiceOffering() { + _vmSnapshotMgr.updateUserVmServiceOffering(userVm, vmSnapshotVO); + verify(_vmSnapshotMgr, never()).changeUserVmServiceOffering(userVm, vmSnapshotVO); + } + + @Test + public void testUpdateUserVmServiceOfferingDifferentServiceOffering() throws ConcurrentOperationException, ResourceUnavailableException, ManagementServerException, VirtualMachineMigrationException { + when(userVm.getServiceOfferingId()).thenReturn(SERVICE_OFFERING_DIFFERENT_ID); + when(_userVmManager.upgradeVirtualMachine(Matchers.eq(TEST_VM_ID), Matchers.eq(SERVICE_OFFERING_ID), mapDetailsCaptor.capture())).thenReturn(true); + _vmSnapshotMgr.updateUserVmServiceOffering(userVm, vmSnapshotVO); + + verify(_vmSnapshotMgr).changeUserVmServiceOffering(userVm, vmSnapshotVO); + verify(_vmSnapshotMgr).getVmMapDetails(userVm); + verify(_vmSnapshotMgr).upgradeUserVmServiceOffering(Matchers.eq(TEST_VM_ID), Matchers.eq(SERVICE_OFFERING_ID), mapDetailsCaptor.capture()); + } + + @Test + public void testGetVmMapDetails() { + Map result = _vmSnapshotMgr.getVmMapDetails(userVm); + assert(result.containsKey(userVmDetailCpuNumber.getName())); + assert(result.containsKey(userVmDetailMemory.getName())); + assertEquals(userVmDetails.size(), result.size()); + assertEquals(userVmDetailCpuNumber.getValue(), result.get(userVmDetailCpuNumber.getName())); + assertEquals(userVmDetailMemory.getValue(), result.get(userVmDetailMemory.getName())); + } + + @Test + public void testChangeUserVmServiceOffering() throws ConcurrentOperationException, ResourceUnavailableException, ManagementServerException, VirtualMachineMigrationException { + when(_userVmManager.upgradeVirtualMachine(Matchers.eq(TEST_VM_ID), Matchers.eq(SERVICE_OFFERING_ID), mapDetailsCaptor.capture())).thenReturn(true); + _vmSnapshotMgr.changeUserVmServiceOffering(userVm, vmSnapshotVO); + verify(_vmSnapshotMgr).getVmMapDetails(userVm); + verify(_vmSnapshotMgr).upgradeUserVmServiceOffering(Matchers.eq(TEST_VM_ID), Matchers.eq(SERVICE_OFFERING_ID), mapDetailsCaptor.capture()); + } + + @Test(expected=CloudRuntimeException.class) + public void testChangeUserVmServiceOfferingFailOnUpgradeVMServiceOffering() throws ConcurrentOperationException, ResourceUnavailableException, ManagementServerException, VirtualMachineMigrationException { + when(_userVmManager.upgradeVirtualMachine(Matchers.eq(TEST_VM_ID), Matchers.eq(SERVICE_OFFERING_ID), mapDetailsCaptor.capture())).thenReturn(false); + _vmSnapshotMgr.changeUserVmServiceOffering(userVm, vmSnapshotVO); + verify(_vmSnapshotMgr).getVmMapDetails(userVm); + verify(_vmSnapshotMgr).upgradeUserVmServiceOffering(Matchers.eq(TEST_VM_ID), Matchers.eq(SERVICE_OFFERING_ID), mapDetailsCaptor.capture()); + } + + @Test + public void testUpgradeUserVmServiceOffering() throws ConcurrentOperationException, ResourceUnavailableException, ManagementServerException, VirtualMachineMigrationException { + Map details = new HashMap() {{ + put(userVmDetailCpuNumber.getName(), userVmDetailCpuNumber.getValue()); + put(userVmDetailMemory.getName(), userVmDetailMemory.getValue()); + }}; + when(_userVmManager.upgradeVirtualMachine(TEST_VM_ID, SERVICE_OFFERING_ID, details)).thenReturn(true); + _vmSnapshotMgr.upgradeUserVmServiceOffering(TEST_VM_ID, SERVICE_OFFERING_ID, details); + + verify(_userVmManager).upgradeVirtualMachine(TEST_VM_ID, SERVICE_OFFERING_ID, details); + } + + @Test + public void testRevertUserVmDetailsFromVmSnapshotNotDynamicServiceOffering() { + _vmSnapshotMgr.revertUserVmDetailsFromVmSnapshot(vmMock, vmSnapshotVO); + verify(_vmSnapshotDetailsDao, never()).listDetails(anyLong()); + } + + @Test + public void testRevertUserVmDetailsFromVmSnapshotDynamicServiceOffering() { + when(serviceOffering.isDynamic()).thenReturn(true); + _vmSnapshotMgr.revertUserVmDetailsFromVmSnapshot(vmMock, vmSnapshotVO); + verify(_vmSnapshotDetailsDao).listDetails(VM_SNAPSHOT_ID); + verify(_userVmDetailsDao).saveDetails(listUserVmDetailsCaptor.capture()); + } + } diff --git a/setup/db/db/schema-4920to41000.sql b/setup/db/db/schema-4920to41000.sql index d743b39ec53a..59fce8af5dd5 100644 --- a/setup/db/db/schema-4920to41000.sql +++ b/setup/db/db/schema-4920to41000.sql @@ -215,3 +215,17 @@ VIEW `image_store_view` AS FROM (`image_store` LEFT JOIN `data_center` ON ((`image_store`.`data_center_id` = `data_center`.`id`))); + +-- Add service_offering_id column to vm_snapshots table +ALTER TABLE `cloud`.`vm_snapshots` ADD COLUMN `service_offering_id` BIGINT(20) UNSIGNED NOT NULL COMMENT '' AFTER `domain_id`; +UPDATE `cloud`.`vm_snapshots` s JOIN `cloud`.`vm_instance` v ON v.id = s.vm_id SET s.service_offering_id = v.service_offering_id; +ALTER TABLE `cloud`.`vm_snapshots` ADD CONSTRAINT `fk_vm_snapshots_service_offering_id` FOREIGN KEY (`service_offering_id`) REFERENCES `cloud`.`service_offering` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- Update vm snapshot details for instances with custom service offerings +INSERT INTO `cloud`.`vm_snapshot_details` (vm_snapshot_id, name, value) +SELECT s.id, d.name, d.value +FROM `cloud`.`user_vm_details` d JOIN `cloud`.`vm_instance` v ON (d.vm_id = v.id) +JOIN `cloud`.`service_offering` o ON (v.service_offering_id = o.id) +JOIN `cloud`.`vm_snapshots` s ON (s.service_offering_id = o.id AND s.vm_id = v.id) +WHERE (o.cpu is null AND o.speed IS NULL AND o.ram_size IS NULL) AND +(d.name = 'cpuNumber' OR d.name = 'cpuSpeed' OR d.name = 'memory'); diff --git a/test/integration/smoke/test_vm_snapshots.py b/test/integration/smoke/test_vm_snapshots.py index f320ea6abdc9..ce1d038bf075 100644 --- a/test/integration/smoke/test_vm_snapshots.py +++ b/test/integration/smoke/test_vm_snapshots.py @@ -16,10 +16,10 @@ # under the License. # Import Local Modules -from marvin.codes import FAILED, KVM, PASS, XEN_SERVER +from marvin.codes import FAILED, KVM, PASS, XEN_SERVER, RUNNING from nose.plugins.attrib import attr from marvin.cloudstackTestCase import cloudstackTestCase -from marvin.lib.utils import random_gen, cleanup_resources, validateList, is_snapshot_on_nfs +from marvin.lib.utils import random_gen, cleanup_resources, validateList, is_snapshot_on_nfs, isAlmostEqual from marvin.lib.base import (Account, ServiceOffering, VirtualMachine, @@ -29,7 +29,8 @@ from marvin.lib.common import (get_zone, get_domain, get_template, - list_snapshots) + list_snapshots, + list_virtual_machines) import time @@ -407,3 +408,256 @@ def test_01_test_vm_volume_snapshot(self): volume_id=volume.id) return + +class Utils: + + def __init__(self): + self.added_service_offerings = { + 'testOffering1' : {'displaytext': 'Test Offering 1', 'cpuspeed': 600, 'cpunumber': 1, 'name': 'Test Offering 1', 'memory': 256}, + 'testOffering2' : {'displaytext': 'Test Offering 2', 'cpuspeed': 600, 'cpunumber': 2, 'name': 'Test Offering 2', 'memory': 512} + } + +class TestChangeServiceOfferingForVmWithSnapshots(cloudstackTestCase): + + @classmethod + def setUpClass(cls): + try: + cls._cleanup = [] + cls.testClient = super(TestChangeServiceOfferingForVmWithSnapshots, cls).getClsTestClient() + cls.api_client = cls.testClient.getApiClient() + cls.services = cls.testClient.getParsedTestDataConfig() + cls.hypervisor = cls.testClient.getHypervisorInfo() + cls.unsupportedHypervisor = False + if cls.hypervisor.lower() in (KVM.lower(), "hyperv", "lxc"): + cls.unsupportedHypervisor = True + return + + cls.domain = get_domain(cls.api_client) + cls.zone = get_zone( + cls.api_client, + cls.testClient.getZoneForTests() + ) + cls.services["small"]["zoneid"] = cls.zone.id + cls.template = get_template( + cls.api_client, + cls.zone.id, + cls.services["ostype"] + ) + if cls.template == FAILED: + assert False, "get_template() failed to return template\ + with description %s" % cls.services["ostype"] + + test_offerings = Utils().added_service_offerings + for offering in test_offerings: + cls.services["service_offerings"][offering] = test_offerings[offering] + + # Create 2 different service offerings + cls.service_offering_1 = ServiceOffering.create( + cls.api_client, + cls.services["service_offerings"]["testOffering1"] + ) + cls._cleanup.append(cls.service_offering_1) + + cls.service_offering_2 = ServiceOffering.create( + cls.api_client, + cls.services["service_offerings"]["testOffering2"] + ) + cls._cleanup.append(cls.service_offering_2) + + cls.account = Account.create( + cls.api_client, + cls.services["account"], + domainid=cls.domain.id + ) + cls._cleanup.append(cls.account) + + except Exception as e: + cls.tearDownClass() + raise Exception("Warning: Exception in setup : %s" % e) + return + + def setUp(self): + self.apiclient = self.testClient.getApiClient() + self.dbclient = self.testClient.getDbConnection() + self.cleanup = [] + + if self.unsupportedHypervisor: + self.skipTest("Skipping test because unsupported hypervisor\ + %s" % self.hypervisor) + + def tearDown(self): + # Clean up, terminate the created resources + cleanup_resources(self.apiclient, self.cleanup) + return + + @classmethod + def tearDownClass(cls): + try: + cleanup_resources(cls.api_client, cls._cleanup) + except Exception as e: + raise Exception("Warning: Exception during cleanup : %s" % e) + + return + + def wait_vm_start(self, apiclient, vmid, timeout, sleep): + while timeout: + vms = VirtualMachine.list(apiclient, id=vmid) + vm_list_validation_result = validateList(vms) + if vm_list_validation_result[0] == PASS and vm_list_validation_result[1].state == RUNNING: + return timeout + time.sleep(sleep) + timeout = timeout - 1 + + return timeout + + def checkCPUAndMemory(self, ssh, service_offering): + cpuinfo = ssh.execute("cat /proc/cpuinfo") + cpu_cnt = len([i for i in cpuinfo if "processor" in i]) + # 'cpu MHz\t\t: 2660.499' + cpu_speed = [i for i in cpuinfo if "cpu MHz" in i][0].split()[3] + meminfo = ssh.execute("cat /proc/meminfo") + # MemTotal: 1017464 kB + total_mem = [i for i in meminfo if "MemTotal" in i][0].split()[1] + + self.debug( + "CPU count: %s, CPU Speed: %s, Mem Info: %s" % (cpu_cnt, cpu_speed, total_mem) + ) + self.assertAlmostEqual( + int(cpu_cnt), + service_offering.cpunumber, + "Check CPU Count for service offering" + ) + + range = 40 + if self.hypervisor.lower() == "hyperv": + range = 200 + self.assertTrue( + isAlmostEqual(int(int(total_mem) / 1024), + int(service_offering.memory), + range=range + ), + "Check Memory(kb) for service offering" + ) + + @attr(tags=["advanced", "smoke"], required_hardware="true") + def test_change_service_offering_for_vm_with_snapshots(self): + """Test to change service offering for instances with vm snapshots + """ + + # 1) Create Virtual Machine using service offering 1 + self.debug("Creating VM using Service Offering 1") + virtual_machine = VirtualMachine.create( + self.apiclient, + self.services["small"], + accountid=self.account.name, + domainid=self.account.domainid, + templateid=self.template.id, + zoneid=self.zone.id, + hypervisor=self.hypervisor, + mode=self.zone.networktype, + serviceofferingid=self.service_offering_1.id + ) + + # Verify Service OFfering 1 CPU cores and memory + try: + ssh_client = virtual_machine.get_ssh_client(reconnect=True) + self.checkCPUAndMemory(ssh_client, self.service_offering_1) + except Exception as e: + self.fail("SSH failed for virtual machine: %s - %s" % (virtual_machine.ipaddress, e)) + + # 2) Take VM Snapshot + self.debug("Taking VM Snapshot for VM - ID: %s" % virtual_machine.id) + vm_snapshot = VmSnapshot.create( + self.apiclient, + virtual_machine.id, + ) + + # 3) Stop Virtual Machine + self.debug("Stopping VM - ID: %s" % virtual_machine.id) + try: + virtual_machine.stop(self.apiclient) + except Exception as e: + self.fail("Failed to stop VM: %s" % e) + + # 4) Change service offering for VM with snapshots from Service Offering 1 to Service Offering 2 + self.debug("Changing service offering from Service Offering 1 to Service Offering 2 for VM - ID: %s" % virtual_machine.id) + virtual_machine.change_service_offering(self.apiclient, self.service_offering_2.id) + + # 5) Start VM + self.debug("Starting VM - ID: %s" % virtual_machine.id) + try: + virtual_machine.start(self.apiclient) + except Exception as e: + self.fail("Failed to start virtual machine: %s, %s" % (virtual_machine.name, e)) + + # Wait for vm to start + timeout = self.wait_vm_start(self.apiclient, virtual_machine.id, self.services["timeout"], + self.services["sleep"]) + if timeout == 0: + self.fail("The virtual machine %s failed to start even after %s minutes" + % (virtual_machine.name, self.services["timeout"])) + + list_vm_response = list_virtual_machines( + self.apiclient, + id=virtual_machine.id + ) + self.assertEqual( + isinstance(list_vm_response, list), + True, + "Check list response returns a valid list" + ) + self.assertNotEqual( + len(list_vm_response), + 0, + "Check VM avaliable in List Virtual Machines" + ) + self.assertEqual( + list_vm_response[0].state, + "Running", + "Check virtual machine is in running state" + ) + self.assertEqual( + list_vm_response[0].id, + virtual_machine.id, + "Check virtual machine id" + ) + + # 6) Verify service offering has changed + try: + ssh_client_2 = virtual_machine.get_ssh_client(reconnect=True) + self.checkCPUAndMemory(ssh_client_2, self.service_offering_2) + except Exception as e: + self.fail("SSH failed for virtual machine: %s - %s" % (virtual_machine.ipaddress, e)) + + # 7) Stop Virtual Machine + self.debug("Stopping VM - ID: %s" % virtual_machine.id) + try: + virtual_machine.stop(self.apiclient) + except Exception as e: + self.fail("Failed to stop VM: %s" % e) + + # 8) Revert to VM Snapshot + self.debug("Revert to vm snapshot: %s" % vm_snapshot.id) + try: + VmSnapshot.revertToSnapshot( + self.apiclient, + vm_snapshot.id + ) + except Exception as e: + self.fail("Failed to revert to VM Snapshot: %s - %s" % (vm_snapshot.id, e)) + + # 9) Start VM + self.debug("Starting VM - ID: %s" % virtual_machine.id) + try: + virtual_machine.start(self.apiclient) + except Exception as e: + self.fail("Failed to start virtual machine: %s, %s" % (virtual_machine.name, e)) + + # 10) Verify service offering has changed to Service Offering 1 (from VM Snapshot) + try: + ssh_client_3 = virtual_machine.get_ssh_client(reconnect=True) + self.checkCPUAndMemory(ssh_client_3, self.service_offering_1) + except Exception as e: + self.fail("SSH failed for virtual machine: %s - %s" % (virtual_machine.ipaddress, e)) + + return \ No newline at end of file