diff --git a/api/src/main/java/com/cloud/vm/VirtualMachine.java b/api/src/main/java/com/cloud/vm/VirtualMachine.java index 7a0715f1ec93..a44c2fbd8080 100644 --- a/api/src/main/java/com/cloud/vm/VirtualMachine.java +++ b/api/src/main/java/com/cloud/vm/VirtualMachine.java @@ -53,7 +53,8 @@ public enum State { Stopping(true, "VM is being stopped. host id has the host that it is being stopped on."), Stopped(false, "VM is stopped. host id should be null."), Destroyed(false, "VM is marked for destroy."), - Expunging(true, "VM is being expunged."), + Expunging(true, "VM is being expunged."), + Expunged(false, "VM is expunged"), Migrating(true, "VM is being migrated. host id holds to from host"), Error(false, "VM is in error"), Unknown(false, "VM state is unknown."), @@ -122,7 +123,8 @@ public static StateMachine2 getStat s_fsm.addTransition(new Transition(State.Stopping, VirtualMachine.Event.AgentReportStopped, State.Stopped, Arrays.asList(new Impact[]{Impact.USAGE}))); s_fsm.addTransition(new Transition(State.Stopping, VirtualMachine.Event.StopRequested, State.Stopping, null)); s_fsm.addTransition(new Transition(State.Stopping, VirtualMachine.Event.AgentReportShutdowned, State.Stopped, Arrays.asList(new Impact[]{Impact.USAGE}))); - s_fsm.addTransition(new Transition(State.Expunging, VirtualMachine.Event.OperationFailed, State.Expunging,null)); + s_fsm.addTransition(new Transition<>(State.Expunging, VirtualMachine.Event.OperationSucceeded, State.Expunged, null)); + s_fsm.addTransition(new Transition<>(State.Expunging, VirtualMachine.Event.OperationFailed, State.Error,null)); s_fsm.addTransition(new Transition(State.Expunging, VirtualMachine.Event.ExpungeOperation, State.Expunging,null)); s_fsm.addTransition(new Transition(State.Error, VirtualMachine.Event.DestroyRequested, State.Expunging, null)); s_fsm.addTransition(new Transition(State.Error, VirtualMachine.Event.ExpungeOperation, State.Expunging, null)); @@ -186,6 +188,10 @@ public static boolean isVmDestroyed(State oldState, Event e, State newState) { return false; } + + public static boolean isVmExpungingOrExpunged(State state) { + return State.Expunging.equals(state) || State.Expunged.equals(state); + } } static final Set systemVMs = new HashSet<>(Arrays.asList(VirtualMachine.Type.ConsoleProxy, VirtualMachine.Type.SecondaryStorageVm)); diff --git a/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java b/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java index 1ecee8ecfda6..b72c51a9b404 100755 --- a/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java +++ b/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java @@ -561,7 +561,20 @@ public void expunge(final String vmUuid) throws ResourceUnavailableException { @Override public void advanceExpunge(final String vmUuid) throws ResourceUnavailableException, OperationTimedoutException, ConcurrentOperationException { final VMInstanceVO vm = _vmDao.findByUuid(vmUuid); - advanceExpunge(vm); + boolean result = false; + try { + advanceExpunge(vm); + result = true; + } finally { + if (vm != null && !State.Expunged.equals(vm.getState())) { + Event event = result ? Event.OperationSucceeded : Event.OperationFailed; + try { + stateTransitTo(_vmDao.findByUuid(vmUuid), event, null, null); + } catch (final NoTransitionException e) { + s_logger.warn(e.getMessage()); + } + } + } } private boolean isValidSystemVMType(VirtualMachine vm) { @@ -576,6 +589,12 @@ protected void advanceExpunge(VMInstanceVO vm) throws ResourceUnavailableExcepti } return; } + if (State.Expunged.equals(vm.getState())) { + if (s_logger.isDebugEnabled()) { + s_logger.debug("vm has already been expunged: " + vm); + } + return; + } advanceStop(vm.getUuid(), VmDestroyForcestop.value()); vm = _vmDao.findByUuid(vm.getUuid()); @@ -1948,7 +1967,7 @@ private void advanceStop(final VMInstanceVO vm, final boolean cleanUpEvenIfUnabl return; } - if (state == State.Destroyed || state == State.Expunging || state == State.Error) { + if (state == State.Destroyed || State.isVmExpungingOrExpunged(state) || state == State.Error) { if (s_logger.isDebugEnabled()) { s_logger.debug("Stopped called on " + vm + " but the state is " + state); } @@ -2160,7 +2179,7 @@ public boolean stateTransitTo(final VirtualMachine vm1, final VirtualMachine.Eve @Override public void destroy(final String vmUuid, final boolean expunge) throws AgentUnavailableException, OperationTimedoutException, ConcurrentOperationException { VMInstanceVO vm = _vmDao.findByUuid(vmUuid); - if (vm == null || vm.getState() == State.Destroyed || vm.getState() == State.Expunging || vm.getRemoved() != null) { + if (vm == null || vm.getState() == State.Destroyed || State.isVmExpungingOrExpunged(vm.getState()) || vm.getRemoved() != null) { if (s_logger.isDebugEnabled()) { s_logger.debug("Unable to find vm or vm is destroyed: " + vm); } @@ -4735,6 +4754,7 @@ private void handlePowerOnReportWithNoPendingJobsOnVM(final VMInstanceVO vm) { case Destroyed: case Expunging: + case Expunged: s_logger.info("Receive power on report when VM is in destroyed or expunging state. vm: " + vm.getId() + ", state: " + vm.getState()); break; @@ -4813,6 +4833,7 @@ private void handlePowerOffReportWithNoPendingJobsOnVM(final VMInstanceVO vm) { case Destroyed: case Expunging: + case Expunged: break; case Error: diff --git a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java index 527d982d72c9..d3b76bce37b9 100644 --- a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java +++ b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java @@ -3189,7 +3189,7 @@ public boolean destroyNetwork(final long networkId, final ReservationContext con final List userVms = _userVmDao.listByNetworkIdAndStates(networkId); for (final UserVmVO vm : userVms) { - if (!(vm.getState() == VirtualMachine.State.Expunging && vm.getRemoved() != null)) { + if (!(VirtualMachine.State.isVmExpungingOrExpunged(vm.getState()) && vm.getRemoved() != null)) { s_logger.warn("Can't delete the network, not all user vms are expunged. Vm " + vm + " is in " + vm.getState() + " state"); return false; } diff --git a/engine/orchestration/src/test/java/com/cloud/vm/VirtualMachineManagerImplTest.java b/engine/orchestration/src/test/java/com/cloud/vm/VirtualMachineManagerImplTest.java index 57f96e4f9de6..80103a4ac6aa 100644 --- a/engine/orchestration/src/test/java/com/cloud/vm/VirtualMachineManagerImplTest.java +++ b/engine/orchestration/src/test/java/com/cloud/vm/VirtualMachineManagerImplTest.java @@ -33,6 +33,9 @@ import java.util.List; import java.util.Map; +import org.apache.cloudstack.annotation.dao.AnnotationDao; +import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService; +import org.apache.cloudstack.engine.orchestration.service.VolumeOrchestrationService; import org.apache.cloudstack.engine.subsystem.api.storage.StoragePoolAllocator; import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; @@ -58,11 +61,16 @@ import com.cloud.deploy.DeploymentPlan; import com.cloud.deploy.DeploymentPlanner; import com.cloud.deploy.DeploymentPlanner.ExcludeList; +import com.cloud.deployasis.dao.UserVmDeployAsIsDetailsDao; import com.cloud.exception.InvalidParameterValueException; +import com.cloud.exception.OperationTimedoutException; +import com.cloud.exception.ResourceUnavailableException; import com.cloud.host.Host; import com.cloud.host.HostVO; import com.cloud.host.dao.HostDao; import com.cloud.hypervisor.Hypervisor.HypervisorType; +import com.cloud.hypervisor.HypervisorGuru; +import com.cloud.hypervisor.HypervisorGuruManager; import com.cloud.service.ServiceOfferingVO; import com.cloud.service.dao.ServiceOfferingDao; import com.cloud.storage.DiskOfferingVO; @@ -75,11 +83,15 @@ import com.cloud.storage.dao.DiskOfferingDao; import com.cloud.storage.dao.StoragePoolHostDao; import com.cloud.storage.dao.VolumeDao; +import com.cloud.utils.Pair; import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.vm.VirtualMachine.State; import com.cloud.vm.dao.UserVmDao; +import com.cloud.vm.dao.UserVmDetailsDao; import com.cloud.vm.dao.VMInstanceDao; +import org.springframework.test.util.ReflectionTestUtils; + @RunWith(MockitoJUnitRunner.class) public class VirtualMachineManagerImplTest { @@ -134,6 +146,26 @@ public class VirtualMachineManagerImplTest { @Mock private DiskOfferingDao diskOfferingDaoMock; + private String vmUuid = "5f515c66-86c6-11ed-81a3-43fdbfd10c20"; + private long vmId = 1000L; + @Mock + private HypervisorGuruManager hvGuruMgrMock; + @Mock + private HypervisorGuru hvGuruMock; + @Mock + private NetworkOrchestrationService networkMgrMock; + @Mock + private VolumeOrchestrationService volumeMgrMock; + @Mock + private VirtualMachineGuru vmGuruMock; + @Mock + private UserVmDetailsDao userVmDetailsDao; + @Mock + private UserVmDeployAsIsDetailsDao userVmDeployAsIsDetailsDao; + @Mock + private AnnotationDao annotationDao; + + @Mock private HostDao hostDaoMock; @Mock @@ -768,4 +800,122 @@ private void prepareAndRunCheckIfNewOfferingStorageScopeMatchesStoragePool(boole Mockito.doReturn(isOfferingUsingLocal).when(diskOfferingMock).isUseLocalStorage(); virtualMachineManagerImpl.checkIfNewOfferingStorageScopeMatchesStoragePool(vmInstanceMock, diskOfferingMock); } + + @Test + public void expungeVmSucceed() throws OperationTimedoutException, ResourceUnavailableException, NoSuchFieldException, IllegalAccessException { + when(vmInstanceDaoMock.findByUuid(vmUuid)).thenReturn(vmInstanceMock); + when(vmInstanceMock.getId()).thenReturn(vmId); + when(vmInstanceMock.getUuid()).thenReturn(vmUuid); + when(vmInstanceMock.getRemoved()).thenReturn(null); + when(vmInstanceMock.getState()).thenReturn(State.Running).thenReturn(State.Stopped).thenReturn(State.Expunging); + when(vmInstanceMock.getHostId()).thenReturn(null); + + final Field f = ConfigKey.class.getDeclaredField("_defaultValue"); + f.setAccessible(true); + f.set(VirtualMachineManagerImpl.VmDestroyForcestop, "false"); + Mockito.doNothing().when(virtualMachineManagerImpl).advanceStop(vmUuid, false); + + when(vmInstanceMock.getHypervisorType()).thenReturn(HypervisorType.Any); + when(hvGuruMgrMock.getGuru(any())).thenReturn(hvGuruMock); + when(hvGuruMock.finalizeExpungeNics(any(), any())).thenReturn(new ArrayList<>()); + Mockito.doNothing().when(networkMgrMock).cleanupNics(any()); + when(hvGuruMock.finalizeExpungeVolumes(any())).thenReturn(new ArrayList<>()); + Mockito.doNothing().when(volumeMgrMock).cleanupVolumes(vmId); + + Map vmGurus = new HashMap<>(); + vmGurus.put(VirtualMachine.Type.User, vmGuruMock); + ReflectionTestUtils.setField(virtualMachineManagerImpl, "_vmGurus", vmGurus); + when(vmInstanceMock.getType()).thenReturn(VirtualMachine.Type.User); + Mockito.doNothing().when(vmGuruMock).finalizeExpunge(any()); + + when(vmInstanceDaoMock.updateState(any(), any(), any(), any(), any())).thenReturn(true); + + virtualMachineManagerImpl.expunge(vmUuid); + + Mockito.verify(vmInstanceDaoMock).updateState(State.Expunging, VirtualMachine.Event.OperationSucceeded, State.Expunged, vmInstanceMock, new Pair<>(null, null)); + } + + @Test(expected = CloudRuntimeException.class) + public void expungeVmFail1() throws OperationTimedoutException, ResourceUnavailableException, NoSuchFieldException, IllegalAccessException { + when(vmInstanceDaoMock.findByUuid(vmUuid)).thenReturn(vmInstanceMock); + when(vmInstanceMock.getUuid()).thenReturn(vmUuid); + when(vmInstanceMock.getRemoved()).thenReturn(null); + when(vmInstanceMock.getState()).thenReturn(State.Running).thenReturn(State.Stopped).thenReturn(State.Expunging); + + final Field f = ConfigKey.class.getDeclaredField("_defaultValue"); + f.setAccessible(true); + f.set(VirtualMachineManagerImpl.VmDestroyForcestop, "false"); + Mockito.doNothing().when(virtualMachineManagerImpl).advanceStop(vmUuid, false); + + when(vmInstanceMock.getHypervisorType()).thenReturn(HypervisorType.Any); + when(hvGuruMgrMock.getGuru(any())).thenReturn(hvGuruMock); + when(hvGuruMock.finalizeExpungeNics(any(), any())).thenReturn(new ArrayList<>()); + Mockito.doThrow(CloudRuntimeException.class).when(networkMgrMock).cleanupNics(any()); + + when(vmInstanceDaoMock.updateState(any(), any(), any(), any(), any())).thenReturn(true); + + virtualMachineManagerImpl.expunge(vmUuid); + + Mockito.verify(vmInstanceDaoMock).updateState(State.Expunging, VirtualMachine.Event.OperationFailed, State.Error, vmInstanceMock, null); + } + + @Test(expected = CloudRuntimeException.class) + public void expungeVmFail2() throws OperationTimedoutException, ResourceUnavailableException, NoSuchFieldException, IllegalAccessException { + when(vmInstanceDaoMock.findByUuid(vmUuid)).thenReturn(vmInstanceMock); + when(vmInstanceMock.getId()).thenReturn(vmId); + when(vmInstanceMock.getUuid()).thenReturn(vmUuid); + when(vmInstanceMock.getRemoved()).thenReturn(null); + when(vmInstanceMock.getState()).thenReturn(State.Running).thenReturn(State.Stopped).thenReturn(State.Expunging); + + final Field f = ConfigKey.class.getDeclaredField("_defaultValue"); + f.setAccessible(true); + f.set(VirtualMachineManagerImpl.VmDestroyForcestop, "false"); + Mockito.doNothing().when(virtualMachineManagerImpl).advanceStop(vmUuid, false); + + when(vmInstanceMock.getHypervisorType()).thenReturn(HypervisorType.Any); + when(hvGuruMgrMock.getGuru(any())).thenReturn(hvGuruMock); + when(hvGuruMock.finalizeExpungeNics(any(), any())).thenReturn(new ArrayList<>()); + Mockito.doNothing().when(networkMgrMock).cleanupNics(any()); + when(hvGuruMock.finalizeExpungeVolumes(any())).thenReturn(new ArrayList<>()); + Mockito.doThrow(CloudRuntimeException.class).when(volumeMgrMock).cleanupVolumes(vmId); + + when(vmInstanceDaoMock.updateState(any(), any(), any(), any(), any())).thenReturn(true); + + virtualMachineManagerImpl.expunge(vmUuid); + + Mockito.verify(vmInstanceDaoMock).updateState(State.Expunging, VirtualMachine.Event.OperationFailed, State.Error, vmInstanceMock, null); + } + + @Test(expected = CloudRuntimeException.class) + public void expungeVmFail3() throws OperationTimedoutException, ResourceUnavailableException, NoSuchFieldException, IllegalAccessException { + when(vmInstanceDaoMock.findByUuid(vmUuid)).thenReturn(vmInstanceMock); + when(vmInstanceMock.getId()).thenReturn(vmId); + when(vmInstanceMock.getUuid()).thenReturn(vmUuid); + when(vmInstanceMock.getRemoved()).thenReturn(null); + when(vmInstanceMock.getState()).thenReturn(State.Running).thenReturn(State.Stopped).thenReturn(State.Expunging); + + final Field f = ConfigKey.class.getDeclaredField("_defaultValue"); + f.setAccessible(true); + f.set(VirtualMachineManagerImpl.VmDestroyForcestop, "false"); + Mockito.doNothing().when(virtualMachineManagerImpl).advanceStop(vmUuid, false); + + when(vmInstanceMock.getHypervisorType()).thenReturn(HypervisorType.Any); + when(hvGuruMgrMock.getGuru(any())).thenReturn(hvGuruMock); + when(hvGuruMock.finalizeExpungeNics(any(), any())).thenReturn(new ArrayList<>()); + Mockito.doNothing().when(networkMgrMock).cleanupNics(any()); + when(hvGuruMock.finalizeExpungeVolumes(any())).thenReturn(new ArrayList<>()); + Mockito.doNothing().when(volumeMgrMock).cleanupVolumes(vmId); + + Map vmGurus = new HashMap<>(); + vmGurus.put(VirtualMachine.Type.User, vmGuruMock); + ReflectionTestUtils.setField(virtualMachineManagerImpl, "_vmGurus", vmGurus); + when(vmInstanceMock.getType()).thenReturn(VirtualMachine.Type.User); + Mockito.doThrow(CloudRuntimeException.class).when(vmGuruMock).finalizeExpunge(any()); + + when(vmInstanceDaoMock.updateState(any(), any(), any(), any(), any())).thenReturn(true); + + virtualMachineManagerImpl.expunge(vmUuid); + + Mockito.verify(vmInstanceDaoMock).updateState(State.Expunging, VirtualMachine.Event.OperationFailed, State.Error, vmInstanceMock, null); + } } diff --git a/engine/schema/src/main/java/com/cloud/vm/VMInstanceVO.java b/engine/schema/src/main/java/com/cloud/vm/VMInstanceVO.java index f3560d68f495..320c9a30fb45 100644 --- a/engine/schema/src/main/java/com/cloud/vm/VMInstanceVO.java +++ b/engine/schema/src/main/java/com/cloud/vm/VMInstanceVO.java @@ -502,7 +502,7 @@ public void setRemoved(Date removed) { @Override public String toString() { - return String.format("VM instance %s", ReflectionToStringBuilderUtils.reflectOnlySelectedFields(this, "id", "instanceName", "uuid", "type")); + return String.format("VM instance %s", ReflectionToStringBuilderUtils.reflectOnlySelectedFields(this, "id", "instanceName", "uuid", "type", "state")); } @Override diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/ConsoleProxyDaoImpl.java b/engine/schema/src/main/java/com/cloud/vm/dao/ConsoleProxyDaoImpl.java index dcf6505ce221..8b79149a9871 100644 --- a/engine/schema/src/main/java/com/cloud/vm/dao/ConsoleProxyDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/vm/dao/ConsoleProxyDaoImpl.java @@ -147,7 +147,7 @@ public boolean remove(Long id) { proxy.setPrivateIpAddress(null); UpdateBuilder ub = getUpdateBuilder(proxy); - ub.set(proxy, "state", State.Destroyed); + ub.set(proxy, "state", State.Expunged); ub.set(proxy, "privateIpAddress", null); update(id, ub, proxy); @@ -182,7 +182,7 @@ public List listByHostId(long hostId) { public List listUpByHostId(long hostId) { SearchCriteria sc = HostUpSearch.create(); sc.setParameters("host", hostId); - sc.setParameters("states", new Object[] {State.Destroyed, State.Stopped, State.Expunging}); + sc.setParameters("states", new Object[] {State.Destroyed, State.Stopped, State.Expunging, State.Expunged}); return listBy(sc); } diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/DomainRouterDaoImpl.java b/engine/schema/src/main/java/com/cloud/vm/dao/DomainRouterDaoImpl.java index 63cdc042b26f..00c8458f5c37 100644 --- a/engine/schema/src/main/java/com/cloud/vm/dao/DomainRouterDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/vm/dao/DomainRouterDaoImpl.java @@ -171,7 +171,7 @@ public boolean remove(final Long id) { final DomainRouterVO router = createForUpdate(); router.setPublicIpAddress(null); final UpdateBuilder ub = getUpdateBuilder(router); - ub.set(router, "state", State.Destroyed); + ub.set(router, "state", State.Expunged); update(id, ub, router); final boolean result = super.remove(id); diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/SecondaryStorageVmDaoImpl.java b/engine/schema/src/main/java/com/cloud/vm/dao/SecondaryStorageVmDaoImpl.java index 2b3c0289b23e..c81f3f3c4057 100644 --- a/engine/schema/src/main/java/com/cloud/vm/dao/SecondaryStorageVmDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/vm/dao/SecondaryStorageVmDaoImpl.java @@ -110,7 +110,7 @@ public boolean remove(Long id) { proxy.setPrivateIpAddress(null); UpdateBuilder ub = getUpdateBuilder(proxy); - ub.set(proxy, "state", State.Destroyed); + ub.set(proxy, "state", State.Expunged); ub.set(proxy, "privateIpAddress", null); update(id, ub, proxy); @@ -155,7 +155,7 @@ public List listByHostId(SecondaryStorageVm.Role role, lon public List listUpByHostId(SecondaryStorageVm.Role role, long hostId) { SearchCriteria sc = HostUpSearch.create(); sc.setParameters("host", hostId); - sc.setParameters("states", new Object[] {State.Destroyed, State.Stopped, State.Expunging}); + sc.setParameters("states", new Object[] {State.Destroyed, State.Stopped, State.Expunging, State.Expunged}); if (role != null) { sc.setParameters("role", role); } diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/UserVmDaoImpl.java b/engine/schema/src/main/java/com/cloud/vm/dao/UserVmDaoImpl.java index 8a1039e83be6..50a57a70f9ad 100644 --- a/engine/schema/src/main/java/com/cloud/vm/dao/UserVmDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/vm/dao/UserVmDaoImpl.java @@ -288,7 +288,7 @@ public void updateVM(long id, String displayName, boolean enable, Long osTypeId, @Override public List findDestroyedVms(Date date) { SearchCriteria sc = DestroySearch.create(); - sc.setParameters("state", State.Destroyed, State.Expunging, State.Error); + sc.setParameters("state", State.Destroyed, State.Expunging, State.Expunged, State.Error); sc.setParameters("updateTime", date); return listBy(sc); @@ -320,7 +320,7 @@ public List listByIsoId(Long isoId) { public List listUpByHostId(Long hostId) { SearchCriteria sc = HostUpSearch.create(); sc.setParameters("host", hostId); - sc.setParameters("states", new Object[] {State.Destroyed, State.Stopped, State.Expunging}); + sc.setParameters("states", new Object[] {State.Destroyed, State.Stopped, State.Expunging, State.Expunged}); return listBy(sc); } @@ -697,9 +697,9 @@ public Long countAllocatedVMsForAccount(long accountId, boolean runningVMsonly) sc.setParameters("account", accountId); sc.setParameters("type", VirtualMachine.Type.User); if (runningVMsonly) - sc.setParameters("state", new Object[] {State.Destroyed, State.Error, State.Expunging, State.Stopped}); + sc.setParameters("state", new Object[] {State.Destroyed, State.Error, State.Expunging, State.Expunged, State.Stopped}); else - sc.setParameters("state", new Object[] {State.Destroyed, State.Error, State.Expunging}); + sc.setParameters("state", new Object[] {State.Destroyed, State.Error, State.Expunging, State.Expunged}); sc.setParameters("displayVm", 1); return customSearch(sc, null).get(0); } diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDaoImpl.java b/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDaoImpl.java index 0e3d4bdde8f1..ec9bac824c1c 100755 --- a/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDaoImpl.java @@ -382,7 +382,7 @@ public List listNonExpungedByTemplate(long templateId) { SearchCriteria sc = TemplateNonExpungedSearch.create(); sc.setParameters("template", templateId); - sc.setParameters("state", State.Expunging); + sc.setParameters("state", State.Expunged); return listBy(sc); } @@ -392,7 +392,7 @@ public List listNonExpungedByZoneAndTemplate(long zoneId, long tem sc.setParameters("zone", zoneId); sc.setParameters("template", templateId); - sc.setParameters("state", State.Expunging); + sc.setParameters("state", State.Expunged); return listBy(sc); } @@ -429,7 +429,7 @@ public List listUpByHostIdTypes(long hostid, Type... types) { SearchCriteria sc = HostIdUpTypesSearch.create(); sc.setParameters("hostid", hostid); sc.setParameters("types", (Object[])types); - sc.setParameters("states", new Object[] {State.Destroyed, State.Stopped, State.Expunging}); + sc.setParameters("states", new Object[] {State.Destroyed, State.Stopped, State.Expunging, State.Expunged}); return listBy(sc); } @@ -601,7 +601,7 @@ public List findIdsOfAllocatedVirtualRoutersForAccount(long accountId) { SearchCriteria sc = FindIdsOfVirtualRoutersByAccount.create(); sc.setParameters("account", accountId); sc.setParameters("type", VirtualMachine.Type.DomainRouter); - sc.setParameters("state", new Object[] {State.Destroyed, State.Error, State.Expunging}); + sc.setParameters("state", new Object[] {State.Destroyed, State.Error, State.Expunging, State.Expunged}); return customSearch(sc, null); } diff --git a/plugins/hypervisors/baremetal/src/main/java/com/cloud/baremetal/manager/BaremetalManagerImpl.java b/plugins/hypervisors/baremetal/src/main/java/com/cloud/baremetal/manager/BaremetalManagerImpl.java index b1aafc692ef1..38a2b96f68b2 100644 --- a/plugins/hypervisors/baremetal/src/main/java/com/cloud/baremetal/manager/BaremetalManagerImpl.java +++ b/plugins/hypervisors/baremetal/src/main/java/com/cloud/baremetal/manager/BaremetalManagerImpl.java @@ -83,7 +83,7 @@ public boolean preStateTransitionEvent(State oldState, Event event, State newSta public boolean postStateTransitionEvent(StateMachine2.Transition transition, VirtualMachine vo, boolean status, Object opaque) { State newState = transition.getToState(); State oldState = transition.getCurrentState(); - if (newState != State.Starting && newState != State.Error && newState != State.Expunging) { + if (newState != State.Starting && newState != State.Error && State.isVmExpungingOrExpunged(newState)) { return true; } diff --git a/plugins/network-elements/juniper-contrail/src/main/java/org/apache/cloudstack/network/contrail/model/VirtualMachineModel.java b/plugins/network-elements/juniper-contrail/src/main/java/org/apache/cloudstack/network/contrail/model/VirtualMachineModel.java index 550bdde1c216..945373f630c5 100644 --- a/plugins/network-elements/juniper-contrail/src/main/java/org/apache/cloudstack/network/contrail/model/VirtualMachineModel.java +++ b/plugins/network-elements/juniper-contrail/src/main/java/org/apache/cloudstack/network/contrail/model/VirtualMachineModel.java @@ -232,6 +232,7 @@ boolean isActiveInstance(VMInstanceVO instance) { case Destroyed: case Error: case Expunging: + case Expunged: return false; default: diff --git a/server/src/main/java/com/cloud/agent/manager/allocator/impl/UserConcentratedAllocator.java b/server/src/main/java/com/cloud/agent/manager/allocator/impl/UserConcentratedAllocator.java index 224514e856d8..1163e65cb227 100644 --- a/server/src/main/java/com/cloud/agent/manager/allocator/impl/UserConcentratedAllocator.java +++ b/server/src/main/java/com/cloud/agent/manager/allocator/impl/UserConcentratedAllocator.java @@ -195,7 +195,7 @@ private boolean dataCenterAndPodHasEnoughCapacity(long dataCenterId, long podId, } private boolean skipCalculation(VMInstanceVO vm) { - if (vm.getState() == State.Expunging) { + if (State.isVmExpungingOrExpunged(vm.getState())) { if (s_logger.isDebugEnabled()) { s_logger.debug("Skip counting capacity for Expunging VM : " + vm.getInstanceName()); } diff --git a/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java b/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java index 6d59b691836c..d5a8f5bb99d1 100644 --- a/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java +++ b/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java @@ -116,9 +116,7 @@ protected UserVmJoinDaoImpl() { public List listActiveByIsoId(Long isoId) { SearchCriteria sc = activeVmByIsoSearch.create(); sc.setParameters("isoId", isoId); - State[] states = new State[2]; - states[0] = State.Error; - states[1] = State.Expunging; + sc.setParameters("stateNotIn", State.Error, State.Expunging, State.Expunged); return listBy(sc); } diff --git a/server/src/main/java/com/cloud/ha/HighAvailabilityManagerImpl.java b/server/src/main/java/com/cloud/ha/HighAvailabilityManagerImpl.java index 1b9b512118d7..9c67e761e8a2 100644 --- a/server/src/main/java/com/cloud/ha/HighAvailabilityManagerImpl.java +++ b/server/src/main/java/com/cloud/ha/HighAvailabilityManagerImpl.java @@ -718,7 +718,7 @@ protected Long destroyVM(final HaWorkVO work) { } try { stopVMWithCleanup(vm, work.getPreviousState()); - if (!VirtualMachine.State.Expunging.equals(work.getPreviousState())) { + if (!VirtualMachine.State.isVmExpungingOrExpunged(work.getPreviousState())) { destroyVM(vm, expunge); return null; } else { diff --git a/server/src/main/java/com/cloud/network/lb/LoadBalancingRulesManagerImpl.java b/server/src/main/java/com/cloud/network/lb/LoadBalancingRulesManagerImpl.java index 3c51e1210b8d..c6fef47634ff 100644 --- a/server/src/main/java/com/cloud/network/lb/LoadBalancingRulesManagerImpl.java +++ b/server/src/main/java/com/cloud/network/lb/LoadBalancingRulesManagerImpl.java @@ -1075,7 +1075,7 @@ public boolean assignToLoadBalancer(long loadBalancerId, List instanceIds, Set passedInstanceIds = vmIdIpMap.keySet(); for (Long instanceId : passedInstanceIds) { UserVm vm = _vmDao.findById(instanceId); - if (vm == null || vm.getState() == State.Destroyed || vm.getState() == State.Expunging) { + if (vm == null || vm.getState() == State.Destroyed || State.isVmExpungingOrExpunged(vm.getState())) { InvalidParameterValueException ex = new InvalidParameterValueException("Invalid instance id specified"); if (vm == null) { ex.addProxyObject(instanceId.toString(), "instanceId"); @@ -2359,6 +2359,7 @@ public Pair, List> listLoadBalancerInstances(List switch (userVm.getState()) { case Destroyed: case Expunging: + case Expunged: case Error: case Unknown: continue; diff --git a/server/src/main/java/com/cloud/network/rules/RulesManagerImpl.java b/server/src/main/java/com/cloud/network/rules/RulesManagerImpl.java index 0feb240743f9..7cd92c185807 100644 --- a/server/src/main/java/com/cloud/network/rules/RulesManagerImpl.java +++ b/server/src/main/java/com/cloud/network/rules/RulesManagerImpl.java @@ -165,7 +165,7 @@ protected void checkIpAndUserVm(IpAddress ipAddress, UserVm userVm, Account call return; } - if (userVm.getState() == VirtualMachine.State.Destroyed || userVm.getState() == VirtualMachine.State.Expunging) { + if (userVm.getState() == VirtualMachine.State.Destroyed || VirtualMachine.State.isVmExpungingOrExpunged(userVm.getState())) { if (!ignoreVmState) { throw new InvalidParameterValueException("Invalid user vm: " + userVm.getId()); } @@ -192,7 +192,7 @@ public void checkRuleAndUserVm(FirewallRule rule, UserVm userVm, Account caller) _accountMgr.checkAccess(caller, null, false, rule, userVm); - if (userVm.getState() == VirtualMachine.State.Destroyed || userVm.getState() == VirtualMachine.State.Expunging) { + if (userVm.getState() == VirtualMachine.State.Destroyed || VirtualMachine.State.isVmExpungingOrExpunged(userVm.getState())) { throw new InvalidParameterValueException("Invalid user vm: " + userVm.getId()); } } @@ -264,7 +264,7 @@ public PortForwardingRule createPortForwardingRule(final PortForwardingRule rule if (vm == null) { throw new InvalidParameterValueException("Unable to create port forwarding rule on address " + ipAddress + ", invalid virtual machine id specified (" + vmId + ")."); - } else if (vm.getState() == VirtualMachine.State.Destroyed || vm.getState() == VirtualMachine.State.Expunging) { + } else if (vm.getState() == VirtualMachine.State.Destroyed || VirtualMachine.State.isVmExpungingOrExpunged(vm.getState())) { throw new InvalidParameterValueException("Invalid user vm: " + vm.getId()); } diff --git a/server/src/main/java/com/cloud/resourcelimit/ResourceLimitManagerImpl.java b/server/src/main/java/com/cloud/resourcelimit/ResourceLimitManagerImpl.java index 27c44f4df2f4..a85225538433 100644 --- a/server/src/main/java/com/cloud/resourcelimit/ResourceLimitManagerImpl.java +++ b/server/src/main/java/com/cloud/resourcelimit/ResourceLimitManagerImpl.java @@ -979,9 +979,9 @@ public long countCpusForAccount(long accountId) { SearchCriteria sc1 = userVmSearch.create(); sc1.setParameters("accountId", accountId); if (VirtualMachineManager.ResourceCountRunningVMsonly.value()) - sc1.setParameters("state", new Object[] {State.Destroyed, State.Error, State.Expunging, State.Stopped}); + sc1.setParameters("state", new Object[] {State.Destroyed, State.Error, State.Expunging, State.Expunged, State.Stopped}); else - sc1.setParameters("state", new Object[] {State.Destroyed, State.Error, State.Expunging}); + sc1.setParameters("state", new Object[] {State.Destroyed, State.Error, State.Expunging, State.Expunged}); sc1.setParameters("displayVm", 1); List userVms = _userVmJoinDao.search(sc1,null); for (UserVmJoinVO vm : userVms) { @@ -1003,9 +1003,9 @@ public long calculateMemoryForAccount(long accountId) { SearchCriteria sc1 = userVmSearch.create(); sc1.setParameters("accountId", accountId); if (VirtualMachineManager.ResourceCountRunningVMsonly.value()) - sc1.setParameters("state", new Object[] {State.Destroyed, State.Error, State.Expunging, State.Stopped}); + sc1.setParameters("state", new Object[] {State.Destroyed, State.Error, State.Expunging, State.Expunged, State.Stopped}); else - sc1.setParameters("state", new Object[] {State.Destroyed, State.Error, State.Expunging}); + sc1.setParameters("state", new Object[] {State.Destroyed, State.Error, State.Expunging, State.Expunged}); sc1.setParameters("displayVm", 1); List userVms = _userVmJoinDao.search(sc1,null); for (UserVmJoinVO vm : userVms) { diff --git a/server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java b/server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java index 6b66b999eadd..619fc6f05071 100755 --- a/server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java +++ b/server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java @@ -1207,7 +1207,7 @@ private boolean supportedByHypervisor(VolumeInfo volume, boolean isFromVmSnapsho if (volume.getInstanceId() != null) { UserVmVO userVm = _vmDao.findById(volume.getInstanceId()); if (userVm != null) { - if (userVm.getState().equals(State.Destroyed) || userVm.getState().equals(State.Expunging)) { + if (userVm.getState().equals(State.Destroyed) || VirtualMachine.State.isVmExpungingOrExpunged(userVm.getState())) { throw new CloudRuntimeException("Creating snapshot failed due to volume:" + volume.getId() + " is associated with vm:" + userVm.getInstanceName() + " is in " + userVm.getState().toString() + " state"); } diff --git a/server/src/main/java/com/cloud/template/TemplateManagerImpl.java b/server/src/main/java/com/cloud/template/TemplateManagerImpl.java index 1a360c88edb8..2c8d9a3320eb 100755 --- a/server/src/main/java/com/cloud/template/TemplateManagerImpl.java +++ b/server/src/main/java/com/cloud/template/TemplateManagerImpl.java @@ -2166,7 +2166,7 @@ private VMTemplateVO updateTemplateOrIso(BaseUpdateTemplateOrIsoCmd cmd) { if (guestOSId != oldGuestOSId) { // vm guest os type need to be updated if template guest os id changes. SearchCriteria sc = _vmInstanceDao.createSearchCriteria(); sc.addAnd("templateId", SearchCriteria.Op.EQ, id); - sc.addAnd("state", SearchCriteria.Op.NEQ, State.Expunging); + sc.addAnd("state", SearchCriteria.Op.NIN, State.Expunging, State.Expunged); List vms = _vmInstanceDao.search(sc, null); if (vms != null && !vms.isEmpty()) { for (VMInstanceVO vm: vms) { diff --git a/server/src/main/java/com/cloud/user/AccountManagerImpl.java b/server/src/main/java/com/cloud/user/AccountManagerImpl.java index 44205bbfa229..7b3796b7045e 100644 --- a/server/src/main/java/com/cloud/user/AccountManagerImpl.java +++ b/server/src/main/java/com/cloud/user/AccountManagerImpl.java @@ -845,7 +845,7 @@ protected boolean cleanupAccount(AccountVO account, long callerUserId, Account c } for (UserVmVO vm : vms) { - if (vm.getState() != VirtualMachine.State.Destroyed && vm.getState() != VirtualMachine.State.Expunging) { + if (vm.getState() != VirtualMachine.State.Destroyed && !VirtualMachine.State.isVmExpungingOrExpunged(vm.getState())) { try { _vmMgr.destroyVm(vm.getId(), false); } catch (Exception e) { diff --git a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java index 0fb51524c900..67759285c068 100644 --- a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java +++ b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java @@ -822,7 +822,7 @@ public UserVm resetVMPassword(ResetVMPasswordCmd cmd, String password) throws Re throw new InvalidParameterValueException("Fail to reset password for the virtual machine, the template is not password enabled"); } - if (userVm.getState() == State.Error || userVm.getState() == State.Expunging) { + if (userVm.getState() == State.Error || State.isVmExpungingOrExpunged(userVm.getState())) { s_logger.error("vm is not in the right state: " + vmId); throw new InvalidParameterValueException("Vm with id " + vmId + " is not in the right state"); } @@ -968,7 +968,7 @@ public UserVm resetVMSSHKey(ResetVMSSHKeyCmd cmd) throws ResourceUnavailableExce // Do parameters input validation - if (userVm.getState() == State.Error || userVm.getState() == State.Expunging) { + if (userVm.getState() == State.Error || State.isVmExpungingOrExpunged(userVm.getState())) { s_logger.error("vm is not in the right state: " + vmId); throw new InvalidParameterValueException("Vm with specified id is not in the right state"); } @@ -1093,7 +1093,7 @@ private UserVm rebootVirtualMachine(long userId, long vmId, boolean enterSetup, s_logger.trace(String.format("reboot %s with enterSetup set to %s", vm.getInstanceName(), Boolean.toString(enterSetup))); } - if (vm == null || vm.getState() == State.Destroyed || vm.getState() == State.Expunging || vm.getRemoved() != null) { + if (vm == null || vm.getState() == State.Destroyed || State.isVmExpungingOrExpunged(vm.getState()) || vm.getRemoved() != null) { s_logger.warn("Vm id=" + vmId + " doesn't exist"); return null; } @@ -2926,7 +2926,7 @@ protected void validateGuestOsIdForUpdateVirtualMachineCommand(UpdateVMCmd cmd) private void saveUsageEvent(UserVmVO vm) { // If vm not destroyed - if( vm.getState() != State.Destroyed && vm.getState() != State.Expunging && vm.getState() != State.Error){ + if( vm.getState() != State.Destroyed && !State.isVmExpungingOrExpunged(vm.getState()) && vm.getState() != State.Error){ if(vm.isDisplayVm()){ //1. Allocated VM Usage Event @@ -2984,7 +2984,7 @@ public UserVm updateVirtualMachine(long id, String displayName, String group, Bo } } - if (vm.getState() == State.Error || vm.getState() == State.Expunging) { + if (vm.getState() == State.Error || State.isVmExpungingOrExpunged(vm.getState())) { s_logger.error("vm is not in the right state: " + id); throw new InvalidParameterValueException("Vm with id " + id + " is not in the right state"); } @@ -3304,7 +3304,7 @@ public UserVm destroyVm(DestroyVMCmd cmd) throws ResourceUnavailableException, C throw new InvalidParameterValueException("unable to find a virtual machine with id " + vmId); } - if (Arrays.asList(State.Destroyed, State.Expunging).contains(vm.getState()) && !expunge) { + if (Arrays.asList(State.Destroyed, State.Expunging, State.Expunged).contains(vm.getState()) && !expunge) { s_logger.debug("Vm id=" + vmId + " is already destroyed"); return vm; } @@ -4258,7 +4258,7 @@ private UserVm getUncheckedUserVmResource(DataCenter zone, String hostName, Stri // If global config vm.instancename.flag is set to true, then CS will set guest VM's name as it appears on the hypervisor, to its hostname. // In case of VMware since VM name must be unique within a DC, check if VM with the same hostname already exists in the zone. VMInstanceVO vmByHostName = _vmInstanceDao.findVMByHostNameInZone(hostName, zone.getId()); - if (vmByHostName != null && vmByHostName.getState() != State.Expunging) { + if (vmByHostName != null && !State.isVmExpungingOrExpunged(vmByHostName.getState())) { throw new InvalidParameterValueException("There already exists a VM by the name: " + hostName + "."); } } else { @@ -4279,7 +4279,7 @@ private UserVm getUncheckedUserVmResource(DataCenter zone, String hostName, Stri // Check if VM with instanceName already exists. VMInstanceVO vmObj = _vmInstanceDao.findVMByInstanceName(instanceName); - if (vmObj != null && vmObj.getState() != State.Expunging) { + if (vmObj != null && !State.isVmExpungingOrExpunged(vmObj.getState())) { throw new InvalidParameterValueException("There already exists a VM by the display name supplied"); } @@ -5554,7 +5554,7 @@ public UserVm destroyVm(long vmId, boolean expunge) throws ResourceUnavailableEx throw ex; } - if (vm.getState() == State.Destroyed || vm.getState() == State.Expunging) { + if (vm.getState() == State.Destroyed || State.isVmExpungingOrExpunged(vm.getState())) { s_logger.trace("Vm id=" + vmId + " is already destroyed"); return vm; } @@ -5745,7 +5745,7 @@ public UserVm expungeVm(long vmId) throws ResourceUnavailableException, Concurre return vm; } - if (!(vm.getState() == State.Destroyed || vm.getState() == State.Expunging || vm.getState() == State.Error)) { + if (!(vm.getState() == State.Destroyed || State.isVmExpungingOrExpunged(vm.getState()) || vm.getState() == State.Error)) { CloudRuntimeException ex = new CloudRuntimeException("Please destroy vm with specified vmId before expunge"); ex.addProxyObject(String.valueOf(vmId), "vmId"); throw ex; @@ -8222,7 +8222,7 @@ private void cleanupUnmanageVMResources(long vmId) { private void unmanageVMFromDB(long vmId) { VMInstanceVO vm = _vmInstanceDao.findById(vmId); userVmDetailsDao.removeDetails(vmId); - vm.setState(State.Expunging); + vm.setState(State.Expunged); vm.setRemoved(new Date()); _vmInstanceDao.update(vm.getId(), vm); } diff --git a/server/src/main/java/com/cloud/vm/UserVmStateListener.java b/server/src/main/java/com/cloud/vm/UserVmStateListener.java index e4df6bbbeb85..72cfda626721 100644 --- a/server/src/main/java/com/cloud/vm/UserVmStateListener.java +++ b/server/src/main/java/com/cloud/vm/UserVmStateListener.java @@ -104,7 +104,7 @@ public boolean postStateTransitionEvent(StateMachine2.Transition t UsageEventUtils.publishUsageEvent(EventTypes.EVENT_NETWORK_OFFERING_REMOVE, vo.getAccountId(), vo.getDataCenterId(), vo.getId(), Long.toString(nic.getId()), network.getNetworkOfferingId(), null, 0L, vo.getClass().getName(), vo.getUuid(), vo.isDisplay()); } - } else if (newState == State.Destroyed || newState == State.Error || newState == State.Expunging) { + } else if (newState == State.Destroyed || newState == State.Error || State.isVmExpungingOrExpunged(newState)) { generateUsageEvent(vo.getServiceOfferingId(), vo, EventTypes.EVENT_VM_DESTROY); } } diff --git a/server/src/main/java/org/apache/cloudstack/affinity/AffinityGroupServiceImpl.java b/server/src/main/java/org/apache/cloudstack/affinity/AffinityGroupServiceImpl.java index 48600ddc0cc7..97bf35906509 100644 --- a/server/src/main/java/org/apache/cloudstack/affinity/AffinityGroupServiceImpl.java +++ b/server/src/main/java/org/apache/cloudstack/affinity/AffinityGroupServiceImpl.java @@ -416,7 +416,7 @@ public boolean postStateTransitionEvent(StateMachine2.Transition t return false; } State newState = transition.getToState(); - if ((newState == State.Expunging) || (newState == State.Error)) { + if (State.isVmExpungingOrExpunged(newState) || newState == State.Error) { // cleanup all affinity groups associations of the Expunged VM SearchCriteria sc = _affinityGroupVMMapDao.createSearchCriteria(); sc.addAnd("instanceId", SearchCriteria.Op.EQ, vo.getId()); diff --git a/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java b/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java index 72ccf621e3a3..0c5e4387ec1d 100644 --- a/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java @@ -1173,7 +1173,7 @@ public UserVmResponse importUnmanagedInstance(ImportUnmanagedInstanceCmd cmd) { // If global config vm.instancename.flag is set to true, then CS will set guest VM's name as it appears on the hypervisor, to its hostname. // In case of VMware since VM name must be unique within a DC, check if VM with the same hostname already exists in the zone. VMInstanceVO vmByHostName = vmDao.findVMByHostNameInZone(hostName, zone.getId()); - if (vmByHostName != null && vmByHostName.getState() != VirtualMachine.State.Expunging) { + if (vmByHostName != null && !VirtualMachine.State.isVmExpungingOrExpunged(vmByHostName.getState())) { throw new InvalidParameterValueException(String.format("Failed to import VM: %s. There already exists a VM by the hostname: %s in zone: %s", instanceName, hostName, zone.getUuid())); } } diff --git a/ui/src/config/section/compute.js b/ui/src/config/section/compute.js index 476b055e7fd1..f9f8e927b3e6 100644 --- a/ui/src/config/section/compute.js +++ b/ui/src/config/section/compute.js @@ -419,7 +419,7 @@ export default { message: (record) => { return record.backupofferingid ? 'message.action.expunge.instance.with.backups' : 'message.action.expunge.instance' }, docHelp: 'adminguide/virtual_machines.html#deleting-vms', dataView: true, - show: (record, store) => { return ['Destroyed', 'Expunging'].includes(record.state) && store.features.allowuserexpungerecovervm } + show: (record, store) => { return ['Destroyed', 'Expunging', 'Expunged'].includes(record.state) && store.features.allowuserexpungerecovervm } }, { api: 'destroyVirtualMachine',