diff --git a/api/src/main/java/com/cloud/storage/Snapshot.java b/api/src/main/java/com/cloud/storage/Snapshot.java index 64917fe64213..9dc7d45b036e 100644 --- a/api/src/main/java/com/cloud/storage/Snapshot.java +++ b/api/src/main/java/com/cloud/storage/Snapshot.java @@ -27,6 +27,7 @@ public interface Snapshot extends ControlledEntity, Identity, InternalIdentity, StateObject { public enum Type { MANUAL, RECURRING, TEMPLATE, HOURLY, DAILY, WEEKLY, MONTHLY, INTERNAL; + // New types should be defined after INTERNAL, and change the max value private int max = 8; public void setMax(int max) { diff --git a/api/src/main/java/com/cloud/storage/VolumeApiService.java b/api/src/main/java/com/cloud/storage/VolumeApiService.java index c9a5139043f6..5eb93ca9556c 100644 --- a/api/src/main/java/com/cloud/storage/VolumeApiService.java +++ b/api/src/main/java/com/cloud/storage/VolumeApiService.java @@ -22,7 +22,6 @@ import java.util.Map; import com.cloud.exception.StorageUnavailableException; -import org.apache.cloudstack.api.command.user.vm.CloneVMCmd; import org.apache.cloudstack.api.command.user.volume.AttachVolumeCmd; import org.apache.cloudstack.api.command.user.volume.CreateVolumeCmd; import org.apache.cloudstack.api.command.user.volume.DetachVolumeCmd; @@ -94,7 +93,7 @@ public interface VolumeApiService { Volume detachVolumeViaDestroyVM(long vmId, long volumeId); - Volume cloneDataVolume(CloneVMCmd cmd, long snapshotId, Volume volume) throws StorageUnavailableException; + Volume cloneDataVolume(long vmId, long snapshotId, Volume volume) throws StorageUnavailableException; Volume detachVolumeFromVM(DetachVolumeCmd cmd); @@ -105,7 +104,7 @@ Snapshot takeSnapshot(Long volumeId, Long policyId, Long snapshotId, Account acc Volume updateVolume(long volumeId, String path, String state, Long storageId, Boolean displayVolume, String customId, long owner, String chainInfo); - Volume attachVolumeToVm(CloneVMCmd cmd, Long volumeId, Long deviceId); + Volume attachVolumeToVM(Long vmId, Long volumeId, Long deviceId); /** * Extracts the volume to a particular location. diff --git a/api/src/main/java/com/cloud/vm/UserVmService.java b/api/src/main/java/com/cloud/vm/UserVmService.java index da8c2373ca79..4eb006c90e04 100644 --- a/api/src/main/java/com/cloud/vm/UserVmService.java +++ b/api/src/main/java/com/cloud/vm/UserVmService.java @@ -100,6 +100,7 @@ public interface UserVmService { void checkCloneCondition(CloneVMCmd cmd) throws ResourceUnavailableException, ConcurrentOperationException, ResourceAllocationException; + void prepareCloneVirtualMachine(CloneVMCmd cmd) throws ResourceAllocationException, InsufficientCapacityException, ResourceUnavailableException; /** * Resets the password of a virtual machine. * diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/CloneVMCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/CloneVMCmd.java index b5e388dbb5e4..af45c6ac810d 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/CloneVMCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/CloneVMCmd.java @@ -17,22 +17,21 @@ import org.apache.cloudstack.api.ApiCommandJobType; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.ApiErrorCode; -import org.apache.cloudstack.api.BaseAsyncCreateCustomIdCmd; +import org.apache.cloudstack.api.BaseAsyncCreateCmd; import org.apache.cloudstack.api.Parameter; import org.apache.cloudstack.api.ResponseObject; import org.apache.cloudstack.api.ServerApiException; import org.apache.cloudstack.api.command.user.UserCmd; import org.apache.cloudstack.api.response.DomainResponse; import org.apache.cloudstack.api.response.UserVmResponse; -//import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.context.CallContext; import org.apache.log4j.Logger; import java.util.Optional; -@APICommand(name = "cloneVirtualMachine", responseObject = UserVmResponse.class, description = "clone a virtual VM in full clone mode", - responseView = ResponseObject.ResponseView.Restricted, requestHasSensitiveInfo = false, responseHasSensitiveInfo = true, entityType = {VirtualMachine.class}) -public class CloneVMCmd extends BaseAsyncCreateCustomIdCmd implements UserCmd { +@APICommand(name = "cloneVirtualMachine", responseObject = UserVmResponse.class, description = "clone a virtual VM", + responseView = ResponseObject.ResponseView.Restricted, requestHasSensitiveInfo = false, responseHasSensitiveInfo = true, entityType = {VirtualMachine.class}, since="4.16.0") +public class CloneVMCmd extends BaseAsyncCreateCmd implements UserCmd { public static final Logger s_logger = Logger.getLogger(CloneVMCmd.class.getName()); private static final String s_name = "clonevirtualmachineresponse"; @@ -102,19 +101,7 @@ public void setTemporaryTemlateId(long tempId) { public void create() throws ResourceAllocationException { try { _userVmService.checkCloneCondition(this); - VirtualMachineTemplate template = _templateService.createPrivateTemplateRecord(this, _accountService.getAccount(getEntityOwnerId()), _volumeService); - if (template == null) { - throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "failed to create a template to db"); - } - s_logger.info("The template id recorded is: " + template.getId()); - setTemporaryTemlateId(template.getId()); - _templateService.createPrivateTemplate(this); - UserVm vmRecord = _userVmService.recordVirtualMachineToDB(this); - if (vmRecord == null) { - throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "unable to record a new VM to db!"); - } - setEntityId(vmRecord.getId()); - setEntityUuid(vmRecord.getUuid()); + _userVmService.prepareCloneVirtualMachine(this); } catch (ResourceUnavailableException | InsufficientCapacityException e) { s_logger.warn("Exception: ", e); @@ -126,11 +113,6 @@ public void create() throws ResourceAllocationException { throw new ServerApiException(e.getErrorCode(), e.getDescription()); } catch (CloudRuntimeException e) { throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, e.getMessage()); - } finally { - if (getTemporarySnapShotId() != null) { - _snapshotService.deleteSnapshot(getTemporarySnapShotId()); - s_logger.warn("clearing the temporary snapshot: " + getTemporarySnapShotId()); - } } } diff --git a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java index 59d0e3aed920..ed978fc57649 100644 --- a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java +++ b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java @@ -32,8 +32,6 @@ import java.util.concurrent.ExecutionException; import javax.inject.Inject; - -import org.apache.cloudstack.api.command.user.vm.CloneVMCmd; import com.cloud.api.query.dao.ServiceOfferingJoinDao; import com.cloud.api.query.vo.ServiceOfferingJoinVO; import org.apache.cloudstack.api.command.user.volume.AttachVolumeCmd; @@ -910,8 +908,7 @@ public VolumeVO createVolume(CreateVolumeCmd cmd) { } @Override - public Volume cloneDataVolume(CloneVMCmd cmd, long snapshotId, Volume volume) throws StorageUnavailableException { - long vmId = cmd.getEntityId(); + public Volume cloneDataVolume(long vmId, long snapshotId, Volume volume) throws StorageUnavailableException { return createVolumeFromSnapshot((VolumeVO) volume, snapshotId, vmId); } @@ -1706,10 +1703,6 @@ private Volume orchestrateAttachVolumeToVM(Long vmId, Long volumeId, Long device } @Override - public Volume attachVolumeToVm(CloneVMCmd cmd, Long volumeId, Long deviceId) { - return attachVolumeToVM(cmd.getEntityId(), volumeId, deviceId); - } - public Volume attachVolumeToVM(Long vmId, Long volumeId, Long deviceId) { Account caller = CallContext.current().getCallingAccount(); diff --git a/server/src/main/java/com/cloud/template/TemplateManagerImpl.java b/server/src/main/java/com/cloud/template/TemplateManagerImpl.java index 188a06766b09..2bc2be2dfd48 100755 --- a/server/src/main/java/com/cloud/template/TemplateManagerImpl.java +++ b/server/src/main/java/com/cloud/template/TemplateManagerImpl.java @@ -1813,6 +1813,12 @@ public VirtualMachineTemplate createPrivateTemplate(CloneVMCmd cmd) throws Cloud s_logger.info("successfully created the template with Id: " + templateId); finalTmpProduct = _tmpltDao.findById(templateId); TemplateDataStoreVO srcTmpltStore = _tmplStoreDao.findByStoreTemplate(store.getId(), templateId); + try { + srcTmpltStore.getSize(); + } catch (NullPointerException e) { + srcTmpltStore.setSize(0L); + _tmplStoreDao.update(srcTmpltStore.getId(), srcTmpltStore); + } UsageEventVO usageEvent = new UsageEventVO(EventTypes.EVENT_TEMPLATE_CREATE, finalTmpProduct.getAccountId(), zoneId, finalTmpProduct.getId(), finalTmpProduct.getName(), null, finalTmpProduct.getSourceTemplateId(), srcTmpltStore.getPhysicalSize(), finalTmpProduct.getSize()); diff --git a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java index f0a680827e60..1d74bd3dee50 100644 --- a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java +++ b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java @@ -532,6 +532,8 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir private BackupDao backupDao; @Inject private BackupManager backupManager; + @Inject + private SnapshotApiService _snapshotService; private ScheduledExecutorService _executor = null; private ScheduledExecutorService _vmIpFetchExecutor = null; @@ -4630,11 +4632,36 @@ private VolumeVO saveDataDiskVolumeFromSnapShot(final Account owner, final Boole }); } + @Override + public void prepareCloneVirtualMachine(CloneVMCmd cmd) throws ResourceAllocationException, ResourceUnavailableException, InsufficientCapacityException { + try { + VirtualMachineTemplate template = _tmplService.createPrivateTemplateRecord(cmd, _accountService.getAccount(cmd.getEntityOwnerId()), _volumeService); + if (template == null) { + throw new CloudRuntimeException("failed to create a template to db"); + } + s_logger.info("The template id recorded is: " + template.getId()); + cmd.setTemporaryTemlateId(template.getId()); + _tmplService.createPrivateTemplate(cmd); + UserVm vmRecord = recordVirtualMachineToDB(cmd); + if (vmRecord == null) { + throw new CloudRuntimeException("Unable to record the VM to DB!"); + } + cmd.setEntityUuid(vmRecord.getUuid()); + cmd.setEntityId(vmRecord.getId()); + } finally { + if (cmd.getTemporarySnapShotId() != null) { + _snapshotService.deleteSnapshot(cmd.getTemporarySnapShotId()); + s_logger.warn("clearing the temporary snapshot: " + cmd.getTemporarySnapShotId()); + } + } + } + @Override @ActionEvent(eventType = EventTypes.EVENT_VM_CLONE, eventDescription = "clone vm", async = true) public Optional cloneVirtualMachine(CloneVMCmd cmd, VolumeApiService volumeService, SnapshotApiService snapshotService) throws ResourceUnavailableException, ConcurrentOperationException, CloudRuntimeException, InsufficientCapacityException, ResourceAllocationException { long vmId = cmd.getEntityId(); UserVmVO curVm = _vmDao.findById(vmId); + Account curVmAccount = _accountDao.findById(curVm.getAccountId()); // create and attach data disk long targetClonedVmId = cmd.getId(); Account caller = CallContext.current().getCallingAccount(); @@ -4669,14 +4696,15 @@ public Optional cloneVirtualMachine(CloneVMCmd cmd, VolumeApiService vol DataCenterVO dataCenter = _dcDao.findById(zoneId); String volumeName = snapshotEntity.getName() + "-DataDisk-Volume"; VolumeVO parentVolume = _volsDao.findByIdIncludingRemoved(snapshotEntity.getVolumeId()); - newDatadisk = saveDataDiskVolumeFromSnapShot(caller, true, zoneId, + newDatadisk = saveDataDiskVolumeFromSnapShot(curVmAccount, true, zoneId, diskOfferingId, provisioningType, size, minIops, maxIops, parentVolume, volumeName, _uuidMgr.generateUuid(Volume.class, null), new HashMap<>()); - VolumeVO volumeEntity = (VolumeVO) volumeService.cloneDataVolume(cmd, snapshotEntity.getId(), newDatadisk); + VolumeVO volumeEntity = (VolumeVO) volumeService.cloneDataVolume(cmd.getEntityId(), snapshotEntity.getId(), newDatadisk); createdVolumes.add(volumeEntity); } for (VolumeVO createdVol : createdVolumes) { - volumeService.attachVolumeToVm(cmd, createdVol.getId(), createdVol.getDeviceId()); +// volumeService.attachVolumeToVm(cmd, createdVol.getId(), createdVol.getDeviceId()); + volumeService.attachVolumeToVM(cmd.getEntityId(), createdVol.getId(), createdVol.getDeviceId()); } } catch (CloudRuntimeException e){ s_logger.warn("data disk process failed during clone, clearing the temporary resources..."); @@ -5755,18 +5783,18 @@ public UserVm recordVirtualMachineToDB(CloneVMCmd cmd) throws ConcurrentOperatio if (dataCenter.getNetworkType() == NetworkType.Basic) { vmResult = createBasicSecurityGroupVirtualMachine(dataCenter, serviceOffering, template, securityGroupIdList, curAccount, hostName, displayName, diskOfferingId, size, group, hypervisorType, cmd.getHttpMethod(), userData, sshKeyPair, ipToNetoworkMap, addr, isDisplayVM, keyboard, affinityGroupIdList, - curVm.getDetails() == null ? new HashMap<>() : curVm.getDetails(), cmd.getCustomId(), new HashMap<>(), + curVm.getDetails() == null ? new HashMap<>() : curVm.getDetails(), null, new HashMap<>(), null, new HashMap<>(), dynamicScalingEnabled); } else { if (dataCenter.isSecurityGroupEnabled()) { vmResult = createAdvancedSecurityGroupVirtualMachine(dataCenter, serviceOffering, template, networkIds, securityGroupIdList, curAccount, hostName, displayName, diskOfferingId, size, group, hypervisorType, cmd.getHttpMethod(), userData, sshKeyPair, ipToNetoworkMap, addr, isDisplayVM, keyboard, - affinityGroupIdList, curVm.getDetails() == null ? new HashMap<>() : curVm.getDetails(), cmd.getCustomId(), new HashMap<>(), + affinityGroupIdList, curVm.getDetails() == null ? new HashMap<>() : curVm.getDetails(), null, new HashMap<>(), null, new HashMap<>(), dynamicScalingEnabled); } else { vmResult = createAdvancedVirtualMachine(dataCenter, serviceOffering, template, networkIds, curAccount, hostName, displayName, diskOfferingId, size, group, hypervisorType, cmd.getHttpMethod(), userData, sshKeyPair, ipToNetoworkMap, addr, isDisplayVM, keyboard, affinityGroupIdList, curVm.getDetails() == null ? new HashMap<>() : curVm.getDetails(), - cmd.getCustomId(), new HashMap<>(), null, new HashMap<>(), dynamicScalingEnabled); + null, new HashMap<>(), null, new HashMap<>(), dynamicScalingEnabled); } } } catch (CloudRuntimeException e) { diff --git a/test/integration/smoke/test_vm_life_cycle.py b/test/integration/smoke/test_vm_life_cycle.py index 61b3a22a6c8e..d853324c1ef0 100644 --- a/test/integration/smoke/test_vm_life_cycle.py +++ b/test/integration/smoke/test_vm_life_cycle.py @@ -876,7 +876,6 @@ def test_11_destroy_vm_and_volumes(self): self.assertEqual(Volume.list(self.apiclient, id=vol1.id), None, "List response contains records when it should not") - class TestSecuredVmMigration(cloudstackTestCase): @classmethod @@ -1943,3 +1942,129 @@ def test_01_vapps_vm_cycle(self): cmd = destroyVirtualMachine.destroyVirtualMachineCmd() cmd.id = vm.id self.apiclient.destroyVirtualMachine(cmd) + +class TestCloneVM(cloudstackTestCase): + + @classmethod + def setUpClass(cls): + testClient = super(TestCloneVM, cls).getClsTestClient() + cls.apiclient = testClient.getApiClient() + cls.services = testClient.getParsedTestDataConfig() + cls.hypervisor = testClient.getHypervisorInfo() + + # Get Zone, Domain and templates + domain = get_domain(cls.apiclient) + cls.zone = get_zone(cls.apiclient, cls.testClient.getZoneForTests()) + cls.services['mode'] = cls.zone.networktype + + # if local storage is enabled, alter the offerings to use localstorage + # this step is needed for devcloud + if cls.zone.localstorageenabled == True: + cls.services["service_offerings"]["tiny"]["storagetype"] = 'local' + cls.services["service_offerings"]["small"]["storagetype"] = 'local' + cls.services["service_offerings"]["medium"]["storagetype"] = 'local' + + template = get_suitable_test_template( + cls.apiclient, + cls.zone.id, + cls.services["ostype"], + cls.hypervisor + ) + if template == FAILED: + assert False, "get_suitable_test_template() failed to return template with description %s" % cls.services["ostype"] + + # Set Zones and disk offerings + cls.services["small"]["zoneid"] = cls.zone.id + cls.services["small"]["template"] = template.id + + cls.services["iso1"]["zoneid"] = cls.zone.id + + # Create VMs, NAT Rules etc + cls.account = Account.create( + cls.apiclient, + cls.services["account"], + domainid=domain.id + ) + + cls.small_offering = ServiceOffering.create( + cls.apiclient, + cls.services["service_offerings"]["small"] + ) + + cls.medium_offering = ServiceOffering.create( + cls.apiclient, + cls.services["service_offerings"]["medium"] + ) + # create small and large virtual machines + cls.small_virtual_machine = VirtualMachine.create( + cls.apiclient, + cls.services["small"], + accountid=cls.account.name, + domainid=cls.account.domainid, + serviceofferingid=cls.small_offering.id, + mode=cls.services["mode"] + ) + cls.medium_virtual_machine = VirtualMachine.create( + cls.apiclient, + cls.services["small"], + accountid=cls.account.name, + domainid=cls.account.domainid, + serviceofferingid=cls.medium_offering.id, + mode=cls.services["mode"] + ) + cls.virtual_machine = VirtualMachine.create( + cls.apiclient, + cls.services["small"], + accountid=cls.account.name, + domainid=cls.account.domainid, + serviceofferingid=cls.small_offering.id, + mode=cls.services["mode"] + ) + cls._cleanup = [ + cls.small_offering, + cls.medium_offering, + cls.account + ] + + @classmethod + def tearDownClass(cls): + super(TestCloneVM, cls).tearDownClass() + + def setUp(self): + self.apiclient = self.testClient.getApiClient() + self.dbclient = self.testClient.getDbConnection() + self.cleanup = [] + + def tearDown(self): + try: + # Clean up, terminate the created ISOs + cleanup_resources(self.apiclient, self.cleanup) + except Exception as e: + raise Exception("Warning: Exception during cleanup : %s" % e) + return + + @attr(tags = ["clone","devcloud", "advanced", "smoke", "basic", "sg"], required_hardware="false") + def test_clone_vm_and_volumes(self): + small_disk_offering = DiskOffering.list(self.apiclient, name='Small')[0]; + small_virtual_machine = VirtualMachine.create( + self.apiclient, + self.services["small"], + accountid=self.account.name, + domainid=self.account.domainid, + serviceofferingid=self.small_offering.id,) + vol1 = Volume.create( + self.apiclient, + self.services, + account=self.account.name, + diskofferingid=small_disk_offering.id, + domainid=self.account.domainid, + zoneid=self.zone.id + ) + small_virtual_machine.attach_volume(self.apiclient, vol1) + self.debug("Clone VM - ID: %s" % small_virtual_machine.id) + try: + clone_response = small_virtual_machine.clone(self.apiclient, small_virtual_machine) + except Exception as e: + self.debug("Clone --" + str(e)) + raise e + self.assertTrue(VirtualMachine.list(self.apiclient, id=clone_response.id) is not None, "vm id should be populated") \ No newline at end of file diff --git a/tools/marvin/marvin/lib/base.py b/tools/marvin/marvin/lib/base.py index 916af64d96cc..9b34cd68ab60 100755 --- a/tools/marvin/marvin/lib/base.py +++ b/tools/marvin/marvin/lib/base.py @@ -744,6 +744,21 @@ def reboot(self, apiclient, forced=None): if response[0] == FAIL: raise Exception(response[1]) + def clone(self, apiclient, vm): + """"Clone the instance""" + cmd = cloneVirtualMachine.cloneVirtualMachineCmd() + cmd.virtualmachineid = vm.id + if vm.id is None: + cmd.virtualmachineid = self.id + response = apiclient.cloneVirtualMachine(cmd) + temp = self.id + self.id = response.id + state = self.getState(apiclient, VirtualMachine.RUNNING) + self.id = temp + if (state[0] == FAIL): + raise Exception(state[1]) + return response + def recover(self, apiclient): """Recover the instance""" cmd = recoverVirtualMachine.recoverVirtualMachineCmd()