diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6a6ea33b14a..42407a48b9b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -54,6 +54,7 @@ jobs: smoke/test_deploy_vm_with_userdata smoke/test_deploy_vms_in_parallel smoke/test_deploy_vms_with_varied_deploymentplanners + smoke/test_restore_vm smoke/test_diagnostics smoke/test_direct_download smoke/test_disk_offerings diff --git a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java index adab257f450..e574d9887c3 100644 --- a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java +++ b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java @@ -7846,7 +7846,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir DiskOffering diskOffering = rootDiskOfferingId != null ? _diskOfferingDao.findById(rootDiskOfferingId) : null; for (VolumeVO root : rootVols) { if ( !Volume.State.Allocated.equals(root.getState()) || newTemplateId != null ) { - _volumeService.validateDestroyVolume(root, caller, expunge, false); + _volumeService.validateDestroyVolume(root, caller, Volume.State.Allocated.equals(root.getState()) || expunge, false); final UserVmVO userVm = vm; Pair vmAndNewVol = Transaction.execute(new TransactionCallbackWithException, CloudRuntimeException>() { @Override @@ -7909,7 +7909,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir // Detach, destroy and create the usage event for the old root volume. _volsDao.detachVolume(root.getId()); - _volumeService.destroyVolume(root.getId(), caller, expunge, false); + _volumeService.destroyVolume(root.getId(), caller, Volume.State.Allocated.equals(root.getState()) || expunge, false); // For VMware hypervisor since the old root volume is replaced by the new root volume, force expunge old root volume if it has been created in storage if (vm.getHypervisorType() == HypervisorType.VMware) { diff --git a/test/integration/smoke/test_restore_vm.py b/test/integration/smoke/test_restore_vm.py new file mode 100644 index 00000000000..ec0383d17a8 --- /dev/null +++ b/test/integration/smoke/test_restore_vm.py @@ -0,0 +1,108 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +""" P1 tests for Scaling up Vm +""" +# Import Local Modules +from marvin.cloudstackTestCase import cloudstackTestCase +from marvin.lib.base import (VirtualMachine, Volume, ServiceOffering, Template) +from marvin.lib.common import (get_zone, get_domain) +from nose.plugins.attrib import attr + +_multiprocess_shared_ = True + + +class TestRestoreVM(cloudstackTestCase): + + @classmethod + def setUpClass(cls): + testClient = super(TestRestoreVM, cls).getClsTestClient() + cls.apiclient = testClient.getApiClient() + cls.services = testClient.getParsedTestDataConfig() + + # Get Zone, Domain and templates + cls.domain = get_domain(cls.apiclient) + cls.zone = get_zone(cls.apiclient, testClient.getZoneForTests()) + cls.hypervisor = testClient.getHypervisorInfo() + cls.services['mode'] = cls.zone.networktype + + cls.services["virtual_machine"]["zoneid"] = cls.zone.id + + cls.service_offering = ServiceOffering.create(cls.apiclient, cls.services["service_offering"]) + + cls.template_t1 = Template.register(cls.apiclient, cls.services["test_templates"][ + cls.hypervisor.lower() if cls.hypervisor.lower() != 'simulator' else 'xenserver'], + zoneid=cls.zone.id, hypervisor=cls.hypervisor.lower()) + + cls.template_t2 = Template.register(cls.apiclient, cls.services["test_templates"][ + cls.hypervisor.lower() if cls.hypervisor.lower() != 'simulator' else 'xenserver'], + zoneid=cls.zone.id, hypervisor=cls.hypervisor.lower()) + + cls._cleanup = [cls.service_offering, cls.template_t1, cls.template_t2] + + @classmethod + def tearDownClass(cls): + super(TestRestoreVM, cls).tearDownClass() + return + + @attr(tags=["advanced", "basic"], required_hardware="false") + def test_01_restore_vm(self): + """Test restore virtual machine + """ + # create a virtual machine + virtual_machine = VirtualMachine.create(self.apiclient, self.services["virtual_machine"], zoneid=self.zone.id, + templateid=self.template_t1.id, + serviceofferingid=self.service_offering.id) + self._cleanup.append(virtual_machine) + + root_vol = Volume.list(self.apiclient, virtualmachineid=virtual_machine.id)[0] + self.assertEqual(root_vol.state, 'Ready', "Volume should be in Ready state") + self.assertEqual(root_vol.size, self.template_t1.size, "Size of volume and template should match") + + virtual_machine.restore(self.apiclient, self.template_t2.id) + restored_vm = VirtualMachine.list(self.apiclient, id=virtual_machine.id)[0] + self.assertEqual(restored_vm.state, 'Running', "VM should be in a running state") + self.assertEqual(restored_vm.templateid, self.template_t2.id, "VM's template after restore is incorrect") + root_vol = Volume.list(self.apiclient, virtualmachineid=restored_vm.id)[0] + self.assertEqual(root_vol.state, 'Ready', "Volume should be in Ready state") + self.assertEqual(root_vol.size, self.template_t2.size, "Size of volume and template should match") + + @attr(tags=["advanced", "basic"], required_hardware="false") + def test_02_restore_vm_allocated_root(self): + """Test restore virtual machine with root disk in allocated state + """ + # create a virtual machine with allocated root disk by setting startvm=False + virtual_machine = VirtualMachine.create(self.apiclient, self.services["virtual_machine"], zoneid=self.zone.id, + templateid=self.template_t1.id, + serviceofferingid=self.service_offering.id, + startvm=False) + self._cleanup.append(virtual_machine) + root_vol = Volume.list(self.apiclient, virtualmachineid=virtual_machine.id)[0] + self.assertEqual(root_vol.state, 'Allocated', "Volume should be in Allocated state") + self.assertEqual(root_vol.size, self.template_t1.size, "Size of volume and template should match") + + virtual_machine.restore(self.apiclient, self.template_t2.id) + restored_vm = VirtualMachine.list(self.apiclient, id=virtual_machine.id)[0] + self.assertEqual(restored_vm.state, 'Stopped', "Check the state of VM") + self.assertEqual(restored_vm.templateid, self.template_t2.id, "Check the template of VM") + + root_vol = Volume.list(self.apiclient, virtualmachineid=restored_vm.id)[0] + self.assertEqual(root_vol.state, 'Allocated', "Volume should be in Allocated state") + self.assertEqual(root_vol.size, self.template_t2.size, "Size of volume and template should match") + + virtual_machine.start(self.apiclient) + root_vol = Volume.list(self.apiclient, virtualmachineid=restored_vm.id)[0] + self.assertEqual(root_vol.state, 'Ready', "Volume should be in Ready state") diff --git a/ui/src/views/compute/ReinstallVm.vue b/ui/src/views/compute/ReinstallVm.vue index ee07011fe28..909f4769425 100644 --- a/ui/src/views/compute/ReinstallVm.vue +++ b/ui/src/views/compute/ReinstallVm.vue @@ -36,7 +36,7 @@ :items="templates" :selected="tabKey" :loading="loading.templates" - :preFillContent="resource.templateid" + :preFillContent="dataPrefill" :key="templateKey" @handle-search-filter="($event) => fetchAllTemplates($event)" @update-template-iso="updateFieldValue" @@ -61,7 +61,7 @@ :zoneId="resource.zoneId" :value="diskOffering ? diskOffering.id : ''" :loading="loading.diskOfferings" - :preFillContent="resource.diskofferingid" + :preFillContent="dataPrefill" :isIsoSelected="false" :isRootDiskOffering="true" @on-selected-disk-size="onSelectDiskSize" @@ -170,7 +170,11 @@ export default { ], diskOffering: {}, diskOfferingCount: 0, - templateKey: 0 + templateKey: 0, + dataPrefill: { + templateid: this.resource.templateid, + diskofferingid: this.resource.diskofferingid + } } }, beforeCreate () { @@ -192,8 +196,10 @@ export default { }, handleSubmit () { const params = { - virtualmachineid: this.resource.id, - templateid: this.templateid + virtualmachineid: this.resource.id + } + if (this.templateid) { + params.templateid = this.templateid } if (this.overrideDiskOffering) { params.diskofferingid = this.diskOffering.id @@ -285,9 +291,11 @@ export default { }, onSelectDiskSize (rowSelected) { this.diskOffering = rowSelected + this.dataPrefill.diskofferingid = rowSelected.id }, updateFieldValue (input, value) { this[input] = value + this.dataPrefill[input] = value } } }