From 4a154b6ca417b6415b731e9a5938228ba8c6e90b Mon Sep 17 00:00:00 2001 From: henko holtzhausen Date: Mon, 9 Jul 2018 12:54:37 +0200 Subject: [PATCH 01/12] WIP volume validation, volume detach --- .../com/cloud/storage/VolumeApiService.java | 9 +- .../apache/cloudstack/api/ApiConstants.java | 2 + .../api/command/user/vm/DestroyVMCmd.java | 31 ++- .../command/user/volume/DetachVolumeCmd.java | 16 +- .../cloud/storage/VolumeApiServiceImpl.java | 142 ++++++------ .../java/com/cloud/vm/UserVmManagerImpl.java | 214 +++++++++++------- ui/l10n/en.js | 2 + ui/scripts/instances.js | 11 + 8 files changed, 257 insertions(+), 170 deletions(-) diff --git a/api/src/main/java/com/cloud/storage/VolumeApiService.java b/api/src/main/java/com/cloud/storage/VolumeApiService.java index cf13cd671a91..ea1ef7173913 100644 --- a/api/src/main/java/com/cloud/storage/VolumeApiService.java +++ b/api/src/main/java/com/cloud/storage/VolumeApiService.java @@ -18,8 +18,8 @@ */ package com.cloud.storage; -import java.net.MalformedURLException; - +import com.cloud.exception.ResourceAllocationException; +import com.cloud.user.Account; 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; @@ -30,8 +30,7 @@ import org.apache.cloudstack.api.command.user.volume.UploadVolumeCmd; import org.apache.cloudstack.api.response.GetUploadParamsResponse; -import com.cloud.exception.ResourceAllocationException; -import com.cloud.user.Account; +import java.net.MalformedURLException; public interface VolumeApiService { /** @@ -79,6 +78,8 @@ public interface VolumeApiService { Volume attachVolumeToVM(AttachVolumeCmd command); + Volume detachVolumesFromVM(long vmId, long volumeId); + Volume detachVolumeFromVM(DetachVolumeCmd cmd); Snapshot takeSnapshot(Long volumeId, Long policyId, Long snapshotId, Account account, boolean quiescevm, Snapshot.LocationType locationType, boolean asyncBackup) diff --git a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java index 504b2149837e..79db9e8e6e9c 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -719,6 +719,8 @@ public class ApiConstants { public static final String LAST_ANNOTATED = "lastannotated"; public static final String LDAP_DOMAIN = "ldapdomain"; + public static final String VOLUMES = "volumes"; + public enum HostDetails { all, capacity, events, stats, min; } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/DestroyVMCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/DestroyVMCmd.java index 730c67767728..6775b56a7575 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/DestroyVMCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/DestroyVMCmd.java @@ -16,10 +16,12 @@ // under the License. package org.apache.cloudstack.api.command.user.vm; -import java.util.List; - -import org.apache.log4j.Logger; - +import com.cloud.event.EventTypes; +import com.cloud.exception.ConcurrentOperationException; +import com.cloud.exception.ResourceUnavailableException; +import com.cloud.user.Account; +import com.cloud.uservm.UserVm; +import com.cloud.vm.VirtualMachine; import org.apache.cloudstack.acl.SecurityChecker.AccessType; import org.apache.cloudstack.api.ACL; import org.apache.cloudstack.api.APICommand; @@ -31,14 +33,11 @@ import org.apache.cloudstack.api.ResponseObject.ResponseView; import org.apache.cloudstack.api.ServerApiException; import org.apache.cloudstack.api.response.UserVmResponse; +import org.apache.cloudstack.api.response.VolumeResponse; import org.apache.cloudstack.context.CallContext; +import org.apache.log4j.Logger; -import com.cloud.event.EventTypes; -import com.cloud.exception.ConcurrentOperationException; -import com.cloud.exception.ResourceUnavailableException; -import com.cloud.user.Account; -import com.cloud.uservm.UserVm; -import com.cloud.vm.VirtualMachine; +import java.util.List; @APICommand(name = "destroyVirtualMachine", description = "Destroys a virtual machine.", responseObject = UserVmResponse.class, responseView = ResponseView.Restricted, entityType = {VirtualMachine.class}, requestHasSensitiveInfo = false, @@ -63,6 +62,14 @@ public class DestroyVMCmd extends BaseAsyncCmd { since = "4.2.1") private Boolean expunge; + @Parameter( name = ApiConstants.VOLUMES, + type = CommandType.LIST, + collectionType = CommandType.UUID, + entityType = VolumeResponse.class, + description = "Comma separated list of volume UUIDs", + since = "4.12.0") + private List volumes; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -78,6 +85,10 @@ public boolean getExpunge() { return expunge; } + public List getVolumes() { + return volumes; + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/volume/DetachVolumeCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/volume/DetachVolumeCmd.java index 55d30e33114e..efa1ac69cb86 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/volume/DetachVolumeCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/volume/DetachVolumeCmd.java @@ -16,27 +16,25 @@ // under the License. package org.apache.cloudstack.api.command.user.volume; -import org.apache.cloudstack.api.BaseAsyncCmd; -import org.apache.log4j.Logger; - +import com.cloud.event.EventTypes; +import com.cloud.storage.Volume; +import com.cloud.user.Account; +import com.cloud.uservm.UserVm; +import com.cloud.vm.VirtualMachine; import org.apache.cloudstack.acl.SecurityChecker.AccessType; import org.apache.cloudstack.api.ACL; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiCommandJobType; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseAsyncCmd; import org.apache.cloudstack.api.Parameter; import org.apache.cloudstack.api.ResponseObject.ResponseView; import org.apache.cloudstack.api.ServerApiException; import org.apache.cloudstack.api.response.UserVmResponse; import org.apache.cloudstack.api.response.VolumeResponse; import org.apache.cloudstack.context.CallContext; - -import com.cloud.event.EventTypes; -import com.cloud.storage.Volume; -import com.cloud.user.Account; -import com.cloud.uservm.UserVm; -import com.cloud.vm.VirtualMachine; +import org.apache.log4j.Logger; @APICommand(name = "detachVolume", description = "Detaches a disk volume from a virtual machine.", responseObject = VolumeResponse.class, responseView = ResponseView.Restricted, entityType = {VirtualMachine.class}, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false) diff --git a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java index 9bff4a17295c..b915cc71f922 100644 --- a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java +++ b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java @@ -16,73 +16,6 @@ // under the License. package com.cloud.storage; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.ArrayList; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; -import java.util.concurrent.ExecutionException; - -import javax.inject.Inject; - -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; -import org.apache.cloudstack.api.command.user.volume.ExtractVolumeCmd; -import org.apache.cloudstack.api.command.user.volume.GetUploadParamsForVolumeCmd; -import org.apache.cloudstack.api.command.user.volume.MigrateVolumeCmd; -import org.apache.cloudstack.api.command.user.volume.ResizeVolumeCmd; -import org.apache.cloudstack.api.command.user.volume.UploadVolumeCmd; -import org.apache.cloudstack.api.response.GetUploadParamsResponse; -import org.apache.cloudstack.context.CallContext; -import org.apache.cloudstack.engine.orchestration.service.VolumeOrchestrationService; -import org.apache.cloudstack.engine.subsystem.api.storage.ChapInfo; -import org.apache.cloudstack.engine.subsystem.api.storage.DataObject; -import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; -import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; -import org.apache.cloudstack.engine.subsystem.api.storage.EndPoint; -import org.apache.cloudstack.engine.subsystem.api.storage.HostScope; -import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStoreInfo; -import org.apache.cloudstack.engine.subsystem.api.storage.Scope; -import org.apache.cloudstack.engine.subsystem.api.storage.StoragePoolAllocator; -import org.apache.cloudstack.engine.subsystem.api.storage.VolumeDataFactory; -import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo; -import org.apache.cloudstack.engine.subsystem.api.storage.VolumeService; -import org.apache.cloudstack.engine.subsystem.api.storage.VolumeService.VolumeApiResult; -import org.apache.cloudstack.framework.async.AsyncCallFuture; -import org.apache.cloudstack.framework.config.ConfigKey; -import org.apache.cloudstack.framework.config.dao.ConfigurationDao; -import org.apache.cloudstack.framework.jobs.AsyncJob; -import org.apache.cloudstack.framework.jobs.AsyncJobExecutionContext; -import org.apache.cloudstack.framework.jobs.AsyncJobManager; -import org.apache.cloudstack.framework.jobs.Outcome; -import org.apache.cloudstack.framework.jobs.dao.VmWorkJobDao; -import org.apache.cloudstack.framework.jobs.impl.AsyncJobVO; -import org.apache.cloudstack.framework.jobs.impl.OutcomeImpl; -import org.apache.cloudstack.framework.jobs.impl.VmWorkJobVO; -import org.apache.cloudstack.jobs.JobInfo; -import org.apache.cloudstack.storage.command.AttachAnswer; -import org.apache.cloudstack.storage.command.AttachCommand; -import org.apache.cloudstack.storage.command.DettachCommand; -import org.apache.cloudstack.storage.command.TemplateOrVolumePostUploadCommand; -import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; -import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailVO; -import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailsDao; -import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; -import org.apache.cloudstack.storage.datastore.db.VolumeDataStoreDao; -import org.apache.cloudstack.storage.datastore.db.VolumeDataStoreVO; -import org.apache.cloudstack.storage.image.datastore.ImageStoreEntity; -import org.apache.cloudstack.utils.identity.ManagementServerNode; -import org.apache.cloudstack.utils.imagestore.ImageStoreUtil; -import org.apache.cloudstack.utils.volume.VirtualMachineDiskInfo; -import org.apache.commons.collections.CollectionUtils; -import org.apache.log4j.Logger; -import org.joda.time.DateTime; -import org.joda.time.DateTimeZone; - import com.cloud.agent.AgentManager; import com.cloud.agent.api.Answer; import com.cloud.agent.api.ModifyTargetsCommand; @@ -175,6 +108,71 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonParseException; +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; +import org.apache.cloudstack.api.command.user.volume.ExtractVolumeCmd; +import org.apache.cloudstack.api.command.user.volume.GetUploadParamsForVolumeCmd; +import org.apache.cloudstack.api.command.user.volume.MigrateVolumeCmd; +import org.apache.cloudstack.api.command.user.volume.ResizeVolumeCmd; +import org.apache.cloudstack.api.command.user.volume.UploadVolumeCmd; +import org.apache.cloudstack.api.response.GetUploadParamsResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.engine.orchestration.service.VolumeOrchestrationService; +import org.apache.cloudstack.engine.subsystem.api.storage.ChapInfo; +import org.apache.cloudstack.engine.subsystem.api.storage.DataObject; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; +import org.apache.cloudstack.engine.subsystem.api.storage.EndPoint; +import org.apache.cloudstack.engine.subsystem.api.storage.HostScope; +import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStoreInfo; +import org.apache.cloudstack.engine.subsystem.api.storage.Scope; +import org.apache.cloudstack.engine.subsystem.api.storage.StoragePoolAllocator; +import org.apache.cloudstack.engine.subsystem.api.storage.VolumeDataFactory; +import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo; +import org.apache.cloudstack.engine.subsystem.api.storage.VolumeService; +import org.apache.cloudstack.engine.subsystem.api.storage.VolumeService.VolumeApiResult; +import org.apache.cloudstack.framework.async.AsyncCallFuture; +import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.framework.config.dao.ConfigurationDao; +import org.apache.cloudstack.framework.jobs.AsyncJob; +import org.apache.cloudstack.framework.jobs.AsyncJobExecutionContext; +import org.apache.cloudstack.framework.jobs.AsyncJobManager; +import org.apache.cloudstack.framework.jobs.Outcome; +import org.apache.cloudstack.framework.jobs.dao.VmWorkJobDao; +import org.apache.cloudstack.framework.jobs.impl.AsyncJobVO; +import org.apache.cloudstack.framework.jobs.impl.OutcomeImpl; +import org.apache.cloudstack.framework.jobs.impl.VmWorkJobVO; +import org.apache.cloudstack.jobs.JobInfo; +import org.apache.cloudstack.storage.command.AttachAnswer; +import org.apache.cloudstack.storage.command.AttachCommand; +import org.apache.cloudstack.storage.command.DettachCommand; +import org.apache.cloudstack.storage.command.TemplateOrVolumePostUploadCommand; +import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; +import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailVO; +import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailsDao; +import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; +import org.apache.cloudstack.storage.datastore.db.VolumeDataStoreDao; +import org.apache.cloudstack.storage.datastore.db.VolumeDataStoreVO; +import org.apache.cloudstack.storage.image.datastore.ImageStoreEntity; +import org.apache.cloudstack.utils.identity.ManagementServerNode; +import org.apache.cloudstack.utils.imagestore.ImageStoreUtil; +import org.apache.cloudstack.utils.volume.VirtualMachineDiskInfo; +import org.apache.commons.collections.CollectionUtils; +import org.apache.log4j.Logger; +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; + +import javax.inject.Inject; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ExecutionException; public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiService, VmWorkJobHandler { private final static Logger s_logger = Logger.getLogger(VolumeApiServiceImpl.class); @@ -1848,6 +1846,16 @@ private void validateRootVolumeDetachAttach(VolumeVO volume, UserVmVO vm) { } } + public Volume detachVolumesFromVM(long vmId, long volumeId) { + + Volume result = orchestrateDetachVolumeFromVM(vmId, volumeId); + + if (result == null) { + throw new CloudRuntimeException("DestroyVM failed - failed to detach volume " + vmId + " from instance " + volumeId); + } + return result; + } + private Volume orchestrateDetachVolumeFromVM(long vmId, long volumeId) { Volume volume = _volsDao.findById(volumeId); diff --git a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java index 86c87ed02b8d..3f4a4a9aa1cb 100644 --- a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java +++ b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java @@ -16,86 +16,6 @@ // under the License. package com.cloud.vm; -import java.io.UnsupportedEncodingException; -import java.net.URLDecoder; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Date; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; - -import javax.inject.Inject; -import javax.naming.ConfigurationException; - -import org.apache.cloudstack.acl.ControlledEntity.ACLType; -import org.apache.cloudstack.acl.SecurityChecker.AccessType; -import org.apache.cloudstack.affinity.AffinityGroupService; -import org.apache.cloudstack.affinity.AffinityGroupVO; -import org.apache.cloudstack.affinity.dao.AffinityGroupDao; -import org.apache.cloudstack.affinity.dao.AffinityGroupVMMapDao; -import org.apache.cloudstack.api.ApiConstants; -import org.apache.cloudstack.api.BaseCmd.HTTPMethod; -import org.apache.cloudstack.api.command.admin.vm.AssignVMCmd; -import org.apache.cloudstack.api.command.admin.vm.RecoverVMCmd; -import org.apache.cloudstack.api.command.user.vm.AddNicToVMCmd; -import org.apache.cloudstack.api.command.user.vm.DeployVMCmd; -import org.apache.cloudstack.api.command.user.vm.DestroyVMCmd; -import org.apache.cloudstack.api.command.user.vm.RebootVMCmd; -import org.apache.cloudstack.api.command.user.vm.RemoveNicFromVMCmd; -import org.apache.cloudstack.api.command.user.vm.ResetVMPasswordCmd; -import org.apache.cloudstack.api.command.user.vm.ResetVMSSHKeyCmd; -import org.apache.cloudstack.api.command.user.vm.RestoreVMCmd; -import org.apache.cloudstack.api.command.user.vm.ScaleVMCmd; -import org.apache.cloudstack.api.command.user.vm.SecurityGroupAction; -import org.apache.cloudstack.api.command.user.vm.StartVMCmd; -import org.apache.cloudstack.api.command.user.vm.UpdateDefaultNicForVMCmd; -import org.apache.cloudstack.api.command.user.vm.UpdateVMCmd; -import org.apache.cloudstack.api.command.user.vm.UpdateVmNicIpCmd; -import org.apache.cloudstack.api.command.user.vm.UpgradeVMCmd; -import org.apache.cloudstack.api.command.user.vmgroup.CreateVMGroupCmd; -import org.apache.cloudstack.api.command.user.vmgroup.DeleteVMGroupCmd; -import org.apache.cloudstack.api.command.user.volume.ResizeVolumeCmd; -import org.apache.cloudstack.context.CallContext; -import org.apache.cloudstack.engine.cloud.entity.api.VirtualMachineEntity; -import org.apache.cloudstack.engine.cloud.entity.api.db.dao.VMNetworkMapDao; -import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService; -import org.apache.cloudstack.engine.orchestration.service.VolumeOrchestrationService; -import org.apache.cloudstack.engine.service.api.OrchestrationService; -import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; -import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; -import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStore; -import org.apache.cloudstack.engine.subsystem.api.storage.VolumeDataFactory; -import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo; -import org.apache.cloudstack.engine.subsystem.api.storage.VolumeService; -import org.apache.cloudstack.engine.subsystem.api.storage.VolumeService.VolumeApiResult; -import org.apache.cloudstack.framework.async.AsyncCallFuture; -import org.apache.cloudstack.framework.config.ConfigKey; -import org.apache.cloudstack.framework.config.Configurable; -import org.apache.cloudstack.framework.config.dao.ConfigurationDao; -import org.apache.cloudstack.managed.context.ManagedContextRunnable; -import org.apache.cloudstack.storage.command.DeleteCommand; -import org.apache.cloudstack.storage.command.DettachCommand; -import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; -import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; -import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreDao; -import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreVO; -import org.apache.commons.codec.binary.Base64; -import org.apache.commons.collections.MapUtils; -import org.apache.commons.lang3.StringUtils; -import org.apache.log4j.Logger; - import com.cloud.agent.AgentManager; import com.cloud.agent.api.Answer; import com.cloud.agent.api.Command; @@ -301,6 +221,84 @@ import com.cloud.vm.snapshot.VMSnapshotManager; import com.cloud.vm.snapshot.VMSnapshotVO; import com.cloud.vm.snapshot.dao.VMSnapshotDao; +import org.apache.cloudstack.acl.ControlledEntity.ACLType; +import org.apache.cloudstack.acl.SecurityChecker.AccessType; +import org.apache.cloudstack.affinity.AffinityGroupService; +import org.apache.cloudstack.affinity.AffinityGroupVO; +import org.apache.cloudstack.affinity.dao.AffinityGroupDao; +import org.apache.cloudstack.affinity.dao.AffinityGroupVMMapDao; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseCmd.HTTPMethod; +import org.apache.cloudstack.api.command.admin.vm.AssignVMCmd; +import org.apache.cloudstack.api.command.admin.vm.RecoverVMCmd; +import org.apache.cloudstack.api.command.user.vm.AddNicToVMCmd; +import org.apache.cloudstack.api.command.user.vm.DeployVMCmd; +import org.apache.cloudstack.api.command.user.vm.DestroyVMCmd; +import org.apache.cloudstack.api.command.user.vm.RebootVMCmd; +import org.apache.cloudstack.api.command.user.vm.RemoveNicFromVMCmd; +import org.apache.cloudstack.api.command.user.vm.ResetVMPasswordCmd; +import org.apache.cloudstack.api.command.user.vm.ResetVMSSHKeyCmd; +import org.apache.cloudstack.api.command.user.vm.RestoreVMCmd; +import org.apache.cloudstack.api.command.user.vm.ScaleVMCmd; +import org.apache.cloudstack.api.command.user.vm.SecurityGroupAction; +import org.apache.cloudstack.api.command.user.vm.StartVMCmd; +import org.apache.cloudstack.api.command.user.vm.UpdateDefaultNicForVMCmd; +import org.apache.cloudstack.api.command.user.vm.UpdateVMCmd; +import org.apache.cloudstack.api.command.user.vm.UpdateVmNicIpCmd; +import org.apache.cloudstack.api.command.user.vm.UpgradeVMCmd; +import org.apache.cloudstack.api.command.user.vmgroup.CreateVMGroupCmd; +import org.apache.cloudstack.api.command.user.vmgroup.DeleteVMGroupCmd; +import org.apache.cloudstack.api.command.user.volume.ResizeVolumeCmd; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.engine.cloud.entity.api.VirtualMachineEntity; +import org.apache.cloudstack.engine.cloud.entity.api.db.dao.VMNetworkMapDao; +import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService; +import org.apache.cloudstack.engine.orchestration.service.VolumeOrchestrationService; +import org.apache.cloudstack.engine.service.api.OrchestrationService; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; +import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStore; +import org.apache.cloudstack.engine.subsystem.api.storage.VolumeDataFactory; +import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo; +import org.apache.cloudstack.engine.subsystem.api.storage.VolumeService; +import org.apache.cloudstack.engine.subsystem.api.storage.VolumeService.VolumeApiResult; +import org.apache.cloudstack.framework.async.AsyncCallFuture; +import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.framework.config.Configurable; +import org.apache.cloudstack.framework.config.dao.ConfigurationDao; +import org.apache.cloudstack.managed.context.ManagedContextRunnable; +import org.apache.cloudstack.storage.command.DeleteCommand; +import org.apache.cloudstack.storage.command.DettachCommand; +import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; +import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; +import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreDao; +import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreVO; +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.collections.MapUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.log4j.Logger; + +import javax.inject.Inject; +import javax.naming.ConfigurationException; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; public class UserVmManagerImpl extends ManagerBase implements UserVmManager, VirtualMachineGuru, UserVmService, Configurable { @@ -2762,6 +2760,26 @@ public UserVm destroyVm(DestroyVMCmd cmd) throws ResourceUnavailableException, C } s_logger.debug("Found no ongoing snapshots on volume of type ROOT, for the vm with id " + vmId); + List volumes = new ArrayList<>(); + + for (Long volId : cmd.getVolumes()) { + VolumeVO vol = _volsDao.findById(volId); + + if (vol == null) { + throw new InvalidParameterValueException("Unable to find volume with ID: " + volId); + } + + volumes.add(vol); + } + + checkForUnattachedVolumes(vmId, volumes); + validateVolumes(volumes); + detachAndDeleteVolumes(volumes); + + if (true) { + throw new CloudRuntimeException("For test purposes"); + } + UserVm destroyedVm = destroyVm(vmId, expunge); if (expunge) { if (!expunge(vm, ctx.getCallingUserId(), ctx.getCallingAccount())) { @@ -6446,4 +6464,40 @@ private boolean checkStatusOfVolumeSnapshots(long vmId, Volume.Type type) { } return false; } + + private void checkForUnattachedVolumes(long vmId, List volumes) { + + StringBuilder sb = new StringBuilder(); + + for (VolumeVO volume : volumes) { + if (vmId != volume.getInstanceId()) { + sb.append(volume.toString() + "; "); + } + } + + if (!StringUtils.isEmpty(sb.toString())) { + throw new InvalidParameterValueException("The following supplied volumes are not attached to the VM: " + sb.toString()); + } + } + + private void validateVolumes(List volumes) { + + for (VolumeVO volume : volumes) { + if (!(volume.getVolumeType() == Volume.Type.ROOT || volume.getVolumeType() == Volume.Type.DATADISK)) { + throw new InvalidParameterValueException("Please specify volume of type " + Volume.Type.DATADISK.toString() + " or " + Volume.Type.ROOT.toString()); + } + } + + // Check that the VM is in the correct state + // Root volume detach is allowed for following hypervisors: Xen/KVM/VmWare + // Don't allow detach if target VM has associated VM snapshots + + } + + private void detachAndDeleteVolumes(List volumes) { + + for (VolumeVO volume : volumes) { + _volumeService.detachVolumesFromVM(volume.getInstanceId(), volume.getId()); + } + } } \ No newline at end of file diff --git a/ui/l10n/en.js b/ui/l10n/en.js index e16031d9e8a3..21e03be598d6 100644 --- a/ui/l10n/en.js +++ b/ui/l10n/en.js @@ -639,6 +639,7 @@ var dictionary = { "label.delete.secondary.staging.store":"Delete Secondary Staging Store", "label.delete.sslcertificate":"Delete SSL Certificate", "label.delete.ucs.manager":"Delete UCS Manager", +"label.delete.volumes":"Delete Volumes", "label.delete.vpn.user":"Delete VPN user", "label.deleting.failed":"Deleting Failed", "label.deleting.processing":"Deleting....", @@ -1807,6 +1808,7 @@ var dictionary = { "label.volgroup":"Volume Group", "label.volume":"Volume", "label.volume.details":"Volume details", +"label.volume.ids":"Volume ID's", "label.volume.limits":"Volume Limits", "label.volume.migrated":"Volume migrated", "label.volume.name":"Volume Name", diff --git a/ui/scripts/instances.js b/ui/scripts/instances.js index 0948db48d071..41811809401e 100644 --- a/ui/scripts/instances.js +++ b/ui/scripts/instances.js @@ -112,6 +112,17 @@ label: 'label.expunge', isBoolean: true, isChecked: false + }, + volumes: { + label: 'label.delete.volumes', + isBoolean: true, + isChecked: false + }, + volumeids: { + label: 'label.volume.ids', + dependsOn: 'volumes', + isBoolean: true, + isHidden: true } } }, From 880b38ea556b41f8213ceeb048ce763edadd3afe Mon Sep 17 00:00:00 2001 From: henko holtzhausen Date: Mon, 9 Jul 2018 16:28:32 +0200 Subject: [PATCH 02/12] WIP delete volume code added. functionally complete, cleanup and refactoring to follow --- server/src/main/java/com/cloud/vm/UserVmManagerImpl.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java index 3f4a4a9aa1cb..c5fc438a8118 100644 --- a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java +++ b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java @@ -2776,10 +2776,6 @@ public UserVm destroyVm(DestroyVMCmd cmd) throws ResourceUnavailableException, C validateVolumes(volumes); detachAndDeleteVolumes(volumes); - if (true) { - throw new CloudRuntimeException("For test purposes"); - } - UserVm destroyedVm = destroyVm(vmId, expunge); if (expunge) { if (!expunge(vm, ctx.getCallingUserId(), ctx.getCallingAccount())) { @@ -6499,5 +6495,9 @@ private void detachAndDeleteVolumes(List volumes) { for (VolumeVO volume : volumes) { _volumeService.detachVolumesFromVM(volume.getInstanceId(), volume.getId()); } + + for (VolumeVO volume : volumes) { + _volumeService.deleteVolume(volume.getId(), CallContext.current().getCallingAccount()); + } } } \ No newline at end of file From eb929a026b88fb32859517f7737608850dd28c4f Mon Sep 17 00:00:00 2001 From: henko holtzhausen Date: Tue, 24 Jul 2018 14:08:55 +0200 Subject: [PATCH 03/12] WIP ui code --- .../java/com/cloud/vm/UserVmManagerImpl.java | 22 ++++--- ui/scripts/instances.js | 47 ++++++++++++--- ui/scripts/ui/dialog.js | 59 +++++++++++++++++++ 3 files changed, 111 insertions(+), 17 deletions(-) diff --git a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java index c5fc438a8118..d08e1e2e9c40 100644 --- a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java +++ b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java @@ -2749,10 +2749,15 @@ public UserVm destroyVm(DestroyVMCmd cmd) throws ResourceUnavailableException, C // check if VM exists UserVmVO vm = _vmDao.findById(vmId); - if (vm == null) { + if (vm == null || vm.getRemoved() != null) { throw new InvalidParameterValueException("unable to find a virtual machine with id " + vmId); } + if (vm.getState() == State.Destroyed || vm.getState() == State.Expunging) { + s_logger.trace("Vm id=" + vmId + " is already destroyed"); + return vm; + } + // check if there are active volume snapshots tasks s_logger.debug("Checking if there are any ongoing snapshots on the ROOT volumes associated with VM with ID " + vmId); if (checkStatusOfVolumeSnapshots(vmId, Volume.Type.ROOT)) { @@ -2762,14 +2767,15 @@ public UserVm destroyVm(DestroyVMCmd cmd) throws ResourceUnavailableException, C List volumes = new ArrayList<>(); - for (Long volId : cmd.getVolumes()) { - VolumeVO vol = _volsDao.findById(volId); + if (cmd.getVolumes() != null) { + for (Long volId : cmd.getVolumes()) { + VolumeVO vol = _volsDao.findById(volId); - if (vol == null) { - throw new InvalidParameterValueException("Unable to find volume with ID: " + volId); + if (vol == null) { + throw new InvalidParameterValueException("Unable to find volume with ID: " + volId); + } + volumes.add(vol); } - - volumes.add(vol); } checkForUnattachedVolumes(vmId, volumes); @@ -6466,7 +6472,7 @@ private void checkForUnattachedVolumes(long vmId, List volumes) { StringBuilder sb = new StringBuilder(); for (VolumeVO volume : volumes) { - if (vmId != volume.getInstanceId()) { + if (volume.getInstanceId() == null || vmId != volume.getInstanceId()) { sb.append(volume.toString() + "; "); } } diff --git a/ui/scripts/instances.js b/ui/scripts/instances.js index 41811809401e..a98041b20857 100644 --- a/ui/scripts/instances.js +++ b/ui/scripts/instances.js @@ -89,6 +89,7 @@ }; var vmDestroyAction = function(args) { + var action = { messages: { notification: function(args) { @@ -122,7 +123,27 @@ label: 'label.volume.ids', dependsOn: 'volumes', isBoolean: true, - isHidden: true + isHidden: true, + multiDataArray: true, + multiData: function(args) { + $.ajax({ + url: createURL("listVolumes&virtualMachineId=" + args.context.instances[0].id) + "&type=DATADISK", +// url: createURL("listVolumes"), + dataType: "json", + async: true, + success: function(json) { + var volumes = json.listvolumesresponse.volume; + args.response.success({ + descriptionField: 'name', + data: volumes + }); + } + }); + } +// multiArray: { +// "id": "1", +// "name": "name" +// } } } }, @@ -137,6 +158,14 @@ expunge: true }); } + if (args.data.volumes == 'on' && args.data.volumeids.length > 0) { + var selectVolumes = args.data.volumeids; + $.extend(data, { + volumes: $(selectVolumes).map(function(index, volume) { + return volume; + }).toArray().join(',') + }); + } $.ajax({ url: createURL('destroyVirtualMachine'), data: data, @@ -696,7 +725,7 @@ if (includingSecurityGroupService == false) { hiddenTabs.push("securityGroups"); } - + if (args.context.instances[0].state == 'Running') { hiddenTabs.push("settings"); } @@ -2288,7 +2317,7 @@ $.extend(dataObj, { networkIds: args.data.network }); - } + } if (args.data.securitygroup != null && args.data.securitygroup != '') { $.extend(dataObj, { securitygroupIds: args.data.securitygroup @@ -3038,7 +3067,7 @@ }); } }, - + /** * Settings tab */ @@ -3089,7 +3118,7 @@ } } newDetails += 'details[0].' + data.name + '=' + data.value; - + $.ajax({ url: createURL('updateVirtualMachine&id=' + args.context.instances[0].id + '&' + newDetails), async:false, @@ -3119,7 +3148,7 @@ args.response.error(parseXMLHttpResponse(json)); } }); - + var detailToDelete = args.data.jsonObj.name; var newDetails = '' for (detail in existingDetails) { @@ -3150,7 +3179,7 @@ add: function(args) { var name = args.data.name; var value = args.data.value; - + var details; $.ajax({ url: createURL('listVirtualMachines&id=' + args.context.instances[0].id), @@ -3164,7 +3193,7 @@ args.response.error(parseXMLHttpResponse(json)); } }); - + var detailsFormat = ''; for (key in details) { detailsFormat += "details[0]." + key + "=" + details[key] + "&"; @@ -3192,7 +3221,7 @@ } } }; - + var parseDetails = function(details) { var listDetails = []; for (detail in details){ diff --git a/ui/scripts/ui/dialog.js b/ui/scripts/ui/dialog.js index c5188592259e..4326c0f3766f 100644 --- a/ui/scripts/ui/dialog.js +++ b/ui/scripts/ui/dialog.js @@ -430,6 +430,65 @@ ); }); + } else if (field.multiDataArray) { + $input = $('
') + .addClass('multi-array').addClass(key).appendTo($value); + + multiArgs = { + context: args.context, + response: { + success: function(args) { + if (args.data == undefined || args.data.length == 0) { + + } else { + $(args.data).each(function() { + + var desc; + if (args.descriptionField) + desc = this[args.descriptionField]; + else + desc = _l(this.description); + + $input.append( + $('
').addClass('item') + .append( + $.merge( + $('
').addClass('name').html(_l(itemValue.label)), + $('
').addClass('value').append( + $('').attr({ + name: itemKey, + type: 'checkbox' + }).appendTo($value) + ) + ) + ) + ); + }); + } + } + } + } + + multiFn = field.multiData; + multiFn(multiArgs); + +// $.each(field.multiArray, function(itemKey, itemValue) { +// $input.append( +// $('
').addClass('item') +// .append( +// $.merge( +// $('
').addClass('name').html(_l(itemValue.label)), +// $('
').addClass('value').append( +// $('').attr({ +// name: itemKey, +// type: 'checkbox' +// }).appendTo($value) +// ) +// ) +// ) +// ); +// }); + } else { $input = $('').attr({ name: key, From 12e627b2d4a10dcb45f03a7a9dabd4ab5d09f000 Mon Sep 17 00:00:00 2001 From: Boris Date: Wed, 25 Jul 2018 15:07:30 +0300 Subject: [PATCH 04/12] Adding a test for DestroyVM with volumes --- test/integration/smoke/test_vm_life_cycle.py | 115 ++++++++++++++++++- 1 file changed, 114 insertions(+), 1 deletion(-) diff --git a/test/integration/smoke/test_vm_life_cycle.py b/test/integration/smoke/test_vm_life_cycle.py index a2daee85c100..7cf87f86e3f4 100644 --- a/test/integration/smoke/test_vm_life_cycle.py +++ b/test/integration/smoke/test_vm_life_cycle.py @@ -32,7 +32,8 @@ Host, Iso, Router, - Configurations) + Configurations, + Volume) from marvin.lib.common import (get_domain, get_zone, get_template, @@ -786,6 +787,118 @@ def test_10_attachAndDetach_iso(self): ) return + @attr(tags = ["devcloud", "advanced", "advancedns", "smoke", "basic", "sg"], required_hardware="false") + def test_11_destroy_vm_and_volumes(self): + """Test destroy Virtual Machine and it's volumes + """ + + # Validate the following + # 1. Deploys a VM and attaches disks to it + # 2. Destroys the VM with DataDisks option + + small_virtual_machine = VirtualMachine.create( + self.apiclient, + self.services["small"], + accountid=self.account.name, + domainid=self.account.domainid, + serviceofferingid=self.small_offering.id, + mode=self.services["mode"] + ) + vol1 = Volume.create( + self.apiclient, + self.services, + account=self.account.name, + domainid=self.account.domainid + ) + + vol2 = Volume.create( + self.apiclient, + self.services, + account=self.account.name, + domainid=self.account.domainid + ) + + small_virtual_machine.attach_volume(self.apiclient, vol1) + small_virtual_machine.attach_volume(self.apiclient, vol2) + + cmd = destroyVirtualMachine.destroyVirtualMachineCmd() + cmd.id = small_virtual_machine.id + cmd.expunge = True + cmd.volumes = vol1.id, vol2.id + + response = self.apiclient.destroyVirtualMachine(cmd) + + self.debug("Destroy VM - ID: %s" % self.small_virtual_machine.id) + self.small_virtual_machine.delete(self.apiclient, expunge=False) + + list_vm_response = VirtualMachine.list( + self.apiclient, + id=self.small_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, + "Destroyed", + "Check virtual machine is in destroyed state" + ) + + list_vol = Volume.list( + self.apiclient, + id=vol1.id + ) + self.assertEqual( + isinstance(list_vol, list), + True, + "Check list response returns a valid list" + ) + + self.assertNotEqual( + len(list_vol), + 0, + "Check VM avaliable in List Virtual Machines" + ) + + self.assertEqual( + list_vol[0].state, + "Destroyed", + "Check virtual machine is in destroyed state" + ) + + list_vol = Volume.list( + self.apiclient, + id=vol2.id + ) + self.assertEqual( + isinstance(list_vol, list), + True, + "Check list response returns a valid list" + ) + + self.assertNotEqual( + len(list_vol), + 0, + "Check VM avaliable in List Virtual Machines" + ) + + self.assertEqual( + list_vol[0].state, + "Destroyed", + "Check virtual machine is in destroyed state" + ) + return + + class TestSecuredVmMigration(cloudstackTestCase): @classmethod From e953db9a98905568193c7725afbe47d05d8ee721 Mon Sep 17 00:00:00 2001 From: henko holtzhausen Date: Tue, 31 Jul 2018 15:10:00 +0200 Subject: [PATCH 05/12] WIP front end complete --- ui/scripts/instances.js | 28 ++++++++++++++++++---------- ui/scripts/ui/dialog.js | 35 +++++++++++++---------------------- 2 files changed, 31 insertions(+), 32 deletions(-) diff --git a/ui/scripts/instances.js b/ui/scripts/instances.js index a98041b20857..4d78e1fd8e13 100644 --- a/ui/scripts/instances.js +++ b/ui/scripts/instances.js @@ -128,22 +128,18 @@ multiData: function(args) { $.ajax({ url: createURL("listVolumes&virtualMachineId=" + args.context.instances[0].id) + "&type=DATADISK", -// url: createURL("listVolumes"), dataType: "json", async: true, success: function(json) { var volumes = json.listvolumesresponse.volume; args.response.success({ descriptionField: 'name', + valueField: 'id', data: volumes }); } }); } -// multiArray: { -// "id": "1", -// "name": "name" -// } } } }, @@ -158,12 +154,24 @@ expunge: true }); } - if (args.data.volumes == 'on' && args.data.volumeids.length > 0) { - var selectVolumes = args.data.volumeids; + if (args.data.volumes == 'on') { + + var regex = RegExp('[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'); + + var selectedVolumes = []; + + for (var key in args.data) { + var matches = key.match(regex); + + if (matches != null) { + selectedVolumes.push(key); + } + } + $.extend(data, { - volumes: $(selectVolumes).map(function(index, volume) { - return volume; - }).toArray().join(',') + volumes: $(selectedVolumes).map(function(index, volume) { + return volume; + }).toArray().join(',') }); } $.ajax({ diff --git a/ui/scripts/ui/dialog.js b/ui/scripts/ui/dialog.js index 4326c0f3766f..069dd0397921 100644 --- a/ui/scripts/ui/dialog.js +++ b/ui/scripts/ui/dialog.js @@ -443,6 +443,12 @@ } else { $(args.data).each(function() { + var id; + if (field.valueField) + id = this[field.valueField]; + else + id = this.id !== undefined ? this.id : this.name; + var desc; if (args.descriptionField) desc = this[args.descriptionField]; @@ -450,15 +456,18 @@ desc = _l(this.description); $input.append( - $('
').addClass('item') + $('
') + .addClass('item') .append( $.merge( - $('
').addClass('name').html(_l(itemValue.label)), + $('
').addClass('name').html(_l(desc)), $('
').addClass('value').append( $('').attr({ - name: itemKey, + name: id, type: 'checkbox' - }).appendTo($value) + }) + .data('json-obj', this) + .appendTo($value) ) ) ) @@ -471,24 +480,6 @@ multiFn = field.multiData; multiFn(multiArgs); - -// $.each(field.multiArray, function(itemKey, itemValue) { -// $input.append( -// $('
').addClass('item') -// .append( -// $.merge( -// $('
').addClass('name').html(_l(itemValue.label)), -// $('
').addClass('value').append( -// $('').attr({ -// name: itemKey, -// type: 'checkbox' -// }).appendTo($value) -// ) -// ) -// ) -// ); -// }); - } else { $input = $('').attr({ name: key, From d2f3b35e70c9057ce7c721bed3dc6a85a77d36cd Mon Sep 17 00:00:00 2001 From: henko holtzhausen Date: Tue, 31 Jul 2018 15:20:07 +0200 Subject: [PATCH 06/12] code cleanup --- server/src/main/java/com/cloud/vm/UserVmManagerImpl.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java index d08e1e2e9c40..101922114bc0 100644 --- a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java +++ b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java @@ -6489,11 +6489,6 @@ private void validateVolumes(List volumes) { throw new InvalidParameterValueException("Please specify volume of type " + Volume.Type.DATADISK.toString() + " or " + Volume.Type.ROOT.toString()); } } - - // Check that the VM is in the correct state - // Root volume detach is allowed for following hypervisors: Xen/KVM/VmWare - // Don't allow detach if target VM has associated VM snapshots - } private void detachAndDeleteVolumes(List volumes) { From 3c7e0ccf4ac568024a3e755d564b3f31b12253d1 Mon Sep 17 00:00:00 2001 From: henko holtzhausen Date: Tue, 31 Jul 2018 15:42:02 +0200 Subject: [PATCH 07/12] improved wording --- ui/l10n/en.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/l10n/en.js b/ui/l10n/en.js index 21e03be598d6..0a6d66a460f5 100644 --- a/ui/l10n/en.js +++ b/ui/l10n/en.js @@ -639,7 +639,7 @@ var dictionary = { "label.delete.secondary.staging.store":"Delete Secondary Staging Store", "label.delete.sslcertificate":"Delete SSL Certificate", "label.delete.ucs.manager":"Delete UCS Manager", -"label.delete.volumes":"Delete Volumes", +"label.delete.volumes":"Volumes to be deleted", "label.delete.vpn.user":"Delete VPN user", "label.deleting.failed":"Deleting Failed", "label.deleting.processing":"Deleting....", From 83ae447dc73ce07dfdc4b786d2659d7fcc6c0eeb Mon Sep 17 00:00:00 2001 From: henko holtzhausen Date: Tue, 31 Jul 2018 18:41:15 +0200 Subject: [PATCH 08/12] changes following code review --- .../com/cloud/storage/VolumeApiService.java | 9 +-- .../apache/cloudstack/api/ApiConstants.java | 2 +- .../api/command/user/vm/DestroyVMCmd.java | 28 ++++---- .../command/user/volume/DetachVolumeCmd.java | 16 +++-- .../cloud/storage/VolumeApiServiceImpl.java | 70 +------------------ .../java/com/cloud/vm/UserVmManagerImpl.java | 22 ++++-- ui/scripts/instances.js | 2 +- 7 files changed, 47 insertions(+), 102 deletions(-) diff --git a/api/src/main/java/com/cloud/storage/VolumeApiService.java b/api/src/main/java/com/cloud/storage/VolumeApiService.java index 4d0c81bc0b8b..91b0bc0712fe 100644 --- a/api/src/main/java/com/cloud/storage/VolumeApiService.java +++ b/api/src/main/java/com/cloud/storage/VolumeApiService.java @@ -18,8 +18,8 @@ */ package com.cloud.storage; -import com.cloud.exception.ResourceAllocationException; -import com.cloud.user.Account; +import java.net.MalformedURLException; + 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; @@ -30,7 +30,8 @@ import org.apache.cloudstack.api.command.user.volume.UploadVolumeCmd; import org.apache.cloudstack.api.response.GetUploadParamsResponse; -import java.net.MalformedURLException; +import com.cloud.exception.ResourceAllocationException; +import com.cloud.user.Account; public interface VolumeApiService { /** @@ -78,7 +79,7 @@ public interface VolumeApiService { Volume attachVolumeToVM(AttachVolumeCmd command); - Volume detachVolumesFromVM(long vmId, long volumeId); + Volume detachVolumeViaDestroyVM(long vmId, long volumeId); Volume detachVolumeFromVM(DetachVolumeCmd cmd); diff --git a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java index 8331ae94c635..76f31b3b43b7 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -726,7 +726,7 @@ public class ApiConstants { public static final String EXITCODE = "exitcode"; public static final String TARGET_ID = "targetid"; - public static final String VOLUMES = "volumes"; + public static final String VOLUME_IDS = "volumeIds"; public enum HostDetails { all, capacity, events, stats, min; diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/DestroyVMCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/DestroyVMCmd.java index 6775b56a7575..15c3d3dd9435 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/DestroyVMCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/DestroyVMCmd.java @@ -16,12 +16,10 @@ // under the License. package org.apache.cloudstack.api.command.user.vm; -import com.cloud.event.EventTypes; -import com.cloud.exception.ConcurrentOperationException; -import com.cloud.exception.ResourceUnavailableException; -import com.cloud.user.Account; -import com.cloud.uservm.UserVm; -import com.cloud.vm.VirtualMachine; +import java.util.List; + +import org.apache.log4j.Logger; + import org.apache.cloudstack.acl.SecurityChecker.AccessType; import org.apache.cloudstack.api.ACL; import org.apache.cloudstack.api.APICommand; @@ -35,9 +33,13 @@ import org.apache.cloudstack.api.response.UserVmResponse; import org.apache.cloudstack.api.response.VolumeResponse; import org.apache.cloudstack.context.CallContext; -import org.apache.log4j.Logger; -import java.util.List; +import com.cloud.event.EventTypes; +import com.cloud.exception.ConcurrentOperationException; +import com.cloud.exception.ResourceUnavailableException; +import com.cloud.user.Account; +import com.cloud.uservm.UserVm; +import com.cloud.vm.VirtualMachine; @APICommand(name = "destroyVirtualMachine", description = "Destroys a virtual machine.", responseObject = UserVmResponse.class, responseView = ResponseView.Restricted, entityType = {VirtualMachine.class}, requestHasSensitiveInfo = false, @@ -62,13 +64,13 @@ public class DestroyVMCmd extends BaseAsyncCmd { since = "4.2.1") private Boolean expunge; - @Parameter( name = ApiConstants.VOLUMES, + @Parameter( name = ApiConstants.VOLUME_IDS, type = CommandType.LIST, collectionType = CommandType.UUID, entityType = VolumeResponse.class, - description = "Comma separated list of volume UUIDs", + description = "Comma separated list of volume UUIDs that will be deleted", since = "4.12.0") - private List volumes; + private List volumeIds; ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// @@ -85,8 +87,8 @@ public boolean getExpunge() { return expunge; } - public List getVolumes() { - return volumes; + public List getVolumeIds() { + return volumeIds; } ///////////////////////////////////////////////////// diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/volume/DetachVolumeCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/volume/DetachVolumeCmd.java index efa1ac69cb86..55d30e33114e 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/volume/DetachVolumeCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/volume/DetachVolumeCmd.java @@ -16,25 +16,27 @@ // under the License. package org.apache.cloudstack.api.command.user.volume; -import com.cloud.event.EventTypes; -import com.cloud.storage.Volume; -import com.cloud.user.Account; -import com.cloud.uservm.UserVm; -import com.cloud.vm.VirtualMachine; +import org.apache.cloudstack.api.BaseAsyncCmd; +import org.apache.log4j.Logger; + import org.apache.cloudstack.acl.SecurityChecker.AccessType; import org.apache.cloudstack.api.ACL; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiCommandJobType; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.ApiErrorCode; -import org.apache.cloudstack.api.BaseAsyncCmd; import org.apache.cloudstack.api.Parameter; import org.apache.cloudstack.api.ResponseObject.ResponseView; import org.apache.cloudstack.api.ServerApiException; import org.apache.cloudstack.api.response.UserVmResponse; import org.apache.cloudstack.api.response.VolumeResponse; import org.apache.cloudstack.context.CallContext; -import org.apache.log4j.Logger; + +import com.cloud.event.EventTypes; +import com.cloud.storage.Volume; +import com.cloud.user.Account; +import com.cloud.uservm.UserVm; +import com.cloud.vm.VirtualMachine; @APICommand(name = "detachVolume", description = "Detaches a disk volume from a virtual machine.", responseObject = VolumeResponse.class, responseView = ResponseView.Restricted, entityType = {VirtualMachine.class}, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false) diff --git a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java index 2dff7380a6ea..dac34e9cce93 100644 --- a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java +++ b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java @@ -176,71 +176,6 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonParseException; -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; -import org.apache.cloudstack.api.command.user.volume.ExtractVolumeCmd; -import org.apache.cloudstack.api.command.user.volume.GetUploadParamsForVolumeCmd; -import org.apache.cloudstack.api.command.user.volume.MigrateVolumeCmd; -import org.apache.cloudstack.api.command.user.volume.ResizeVolumeCmd; -import org.apache.cloudstack.api.command.user.volume.UploadVolumeCmd; -import org.apache.cloudstack.api.response.GetUploadParamsResponse; -import org.apache.cloudstack.context.CallContext; -import org.apache.cloudstack.engine.orchestration.service.VolumeOrchestrationService; -import org.apache.cloudstack.engine.subsystem.api.storage.ChapInfo; -import org.apache.cloudstack.engine.subsystem.api.storage.DataObject; -import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; -import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; -import org.apache.cloudstack.engine.subsystem.api.storage.EndPoint; -import org.apache.cloudstack.engine.subsystem.api.storage.HostScope; -import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStoreInfo; -import org.apache.cloudstack.engine.subsystem.api.storage.Scope; -import org.apache.cloudstack.engine.subsystem.api.storage.StoragePoolAllocator; -import org.apache.cloudstack.engine.subsystem.api.storage.VolumeDataFactory; -import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo; -import org.apache.cloudstack.engine.subsystem.api.storage.VolumeService; -import org.apache.cloudstack.engine.subsystem.api.storage.VolumeService.VolumeApiResult; -import org.apache.cloudstack.framework.async.AsyncCallFuture; -import org.apache.cloudstack.framework.config.ConfigKey; -import org.apache.cloudstack.framework.config.dao.ConfigurationDao; -import org.apache.cloudstack.framework.jobs.AsyncJob; -import org.apache.cloudstack.framework.jobs.AsyncJobExecutionContext; -import org.apache.cloudstack.framework.jobs.AsyncJobManager; -import org.apache.cloudstack.framework.jobs.Outcome; -import org.apache.cloudstack.framework.jobs.dao.VmWorkJobDao; -import org.apache.cloudstack.framework.jobs.impl.AsyncJobVO; -import org.apache.cloudstack.framework.jobs.impl.OutcomeImpl; -import org.apache.cloudstack.framework.jobs.impl.VmWorkJobVO; -import org.apache.cloudstack.jobs.JobInfo; -import org.apache.cloudstack.storage.command.AttachAnswer; -import org.apache.cloudstack.storage.command.AttachCommand; -import org.apache.cloudstack.storage.command.DettachCommand; -import org.apache.cloudstack.storage.command.TemplateOrVolumePostUploadCommand; -import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; -import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailVO; -import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailsDao; -import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; -import org.apache.cloudstack.storage.datastore.db.VolumeDataStoreDao; -import org.apache.cloudstack.storage.datastore.db.VolumeDataStoreVO; -import org.apache.cloudstack.storage.image.datastore.ImageStoreEntity; -import org.apache.cloudstack.utils.identity.ManagementServerNode; -import org.apache.cloudstack.utils.imagestore.ImageStoreUtil; -import org.apache.cloudstack.utils.volume.VirtualMachineDiskInfo; -import org.apache.commons.collections.CollectionUtils; -import org.apache.log4j.Logger; -import org.joda.time.DateTime; -import org.joda.time.DateTimeZone; - -import javax.inject.Inject; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.ArrayList; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; -import java.util.concurrent.ExecutionException; public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiService, VmWorkJobHandler { private final static Logger s_logger = Logger.getLogger(VolumeApiServiceImpl.class); @@ -1914,13 +1849,10 @@ private void validateRootVolumeDetachAttach(VolumeVO volume, UserVmVO vm) { } } - public Volume detachVolumesFromVM(long vmId, long volumeId) { + public Volume detachVolumeViaDestroyVM(long vmId, long volumeId) { Volume result = orchestrateDetachVolumeFromVM(vmId, volumeId); - if (result == null) { - throw new CloudRuntimeException("DestroyVM failed - failed to detach volume " + vmId + " from instance " + volumeId); - } return result; } diff --git a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java index 101922114bc0..06351b41f525 100644 --- a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java +++ b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java @@ -2767,8 +2767,8 @@ public UserVm destroyVm(DestroyVMCmd cmd) throws ResourceUnavailableException, C List volumes = new ArrayList<>(); - if (cmd.getVolumes() != null) { - for (Long volId : cmd.getVolumes()) { + if (cmd.getVolumeIds() != null) { + for (Long volId : cmd.getVolumeIds()) { VolumeVO vol = _volsDao.findById(volId); if (vol == null) { @@ -2780,7 +2780,6 @@ public UserVm destroyVm(DestroyVMCmd cmd) throws ResourceUnavailableException, C checkForUnattachedVolumes(vmId, volumes); validateVolumes(volumes); - detachAndDeleteVolumes(volumes); UserVm destroyedVm = destroyVm(vmId, expunge); if (expunge) { @@ -2789,6 +2788,8 @@ public UserVm destroyVm(DestroyVMCmd cmd) throws ResourceUnavailableException, C } } + detachAndDeleteVolumes(volumes); + return destroyedVm; } @@ -6494,11 +6495,18 @@ private void validateVolumes(List volumes) { private void detachAndDeleteVolumes(List volumes) { for (VolumeVO volume : volumes) { - _volumeService.detachVolumesFromVM(volume.getInstanceId(), volume.getId()); - } - for (VolumeVO volume : volumes) { - _volumeService.deleteVolume(volume.getId(), CallContext.current().getCallingAccount()); + Volume detachResult = _volumeService.detachVolumeViaDestroyVM(volume.getInstanceId(), volume.getId()); + + if (detachResult == null) { + s_logger.error("DestroyVM remove volume - failed to detach and delete volume " + volume.getInstanceId() + " from instance " + volume.getId()); + } + + boolean deleteResult = _volumeService.deleteVolume(volume.getId(), CallContext.current().getCallingAccount()); + + if (!deleteResult) { + s_logger.error("DestroyVM remove volume - failed to delete volume " + volume.getInstanceId() + " from instance " + volume.getId()); + } } } } \ No newline at end of file diff --git a/ui/scripts/instances.js b/ui/scripts/instances.js index 4d78e1fd8e13..2073b682de33 100644 --- a/ui/scripts/instances.js +++ b/ui/scripts/instances.js @@ -169,7 +169,7 @@ } $.extend(data, { - volumes: $(selectedVolumes).map(function(index, volume) { + volumeIds: $(selectedVolumes).map(function(index, volume) { return volume; }).toArray().join(',') }); From 6129eb62abbebb68e65ca309cf58e6ad02af0300 Mon Sep 17 00:00:00 2001 From: henko holtzhausen Date: Wed, 1 Aug 2018 15:11:05 +0200 Subject: [PATCH 09/12] changes following code review --- .../apache/cloudstack/api/ApiConstants.java | 2 +- .../cloud/storage/VolumeApiServiceImpl.java | 1 + .../java/com/cloud/vm/UserVmManagerImpl.java | 39 ++++++++++++------- ui/scripts/instances.js | 2 +- 4 files changed, 28 insertions(+), 16 deletions(-) diff --git a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java index 76f31b3b43b7..5a8cbe546a39 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -726,7 +726,7 @@ public class ApiConstants { public static final String EXITCODE = "exitcode"; public static final String TARGET_ID = "targetid"; - public static final String VOLUME_IDS = "volumeIds"; + public static final String VOLUME_IDS = "volumeids"; public enum HostDetails { all, capacity, events, stats, min; diff --git a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java index dac34e9cce93..04f8edaf9953 100644 --- a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java +++ b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java @@ -1849,6 +1849,7 @@ private void validateRootVolumeDetachAttach(VolumeVO volume, UserVmVO vm) { } } + @ActionEvent(eventType = EventTypes.EVENT_VOLUME_DETACH, eventDescription = "detaching volume") public Volume detachVolumeViaDestroyVM(long vmId, long volumeId) { Volume result = orchestrateDetachVolumeFromVM(vmId, volumeId); diff --git a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java index 06351b41f525..ea09f2b8f319 100644 --- a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java +++ b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java @@ -2765,21 +2765,11 @@ public UserVm destroyVm(DestroyVMCmd cmd) throws ResourceUnavailableException, C } s_logger.debug("Found no ongoing snapshots on volume of type ROOT, for the vm with id " + vmId); - List volumes = new ArrayList<>(); - - if (cmd.getVolumeIds() != null) { - for (Long volId : cmd.getVolumeIds()) { - VolumeVO vol = _volsDao.findById(volId); - - if (vol == null) { - throw new InvalidParameterValueException("Unable to find volume with ID: " + volId); - } - volumes.add(vol); - } - } + List volumes = getVolumesFromIds(cmd); checkForUnattachedVolumes(vmId, volumes); validateVolumes(volumes); + detachVolumesFromVm(volumes); UserVm destroyedVm = destroyVm(vmId, expunge); if (expunge) { @@ -2788,11 +2778,26 @@ public UserVm destroyVm(DestroyVMCmd cmd) throws ResourceUnavailableException, C } } - detachAndDeleteVolumes(volumes); + deleteVolumesFromVm(volumes); return destroyedVm; } + private List getVolumesFromIds(DestroyVMCmd cmd) { + List volumes = new ArrayList<>(); + if (cmd.getVolumeIds() != null) { + for (Long volId : cmd.getVolumeIds()) { + VolumeVO vol = _volsDao.findById(volId); + + if (vol == null) { + throw new InvalidParameterValueException("Unable to find volume with ID: " + volId); + } + volumes.add(vol); + } + } + return volumes; + } + @Override @DB public InstanceGroupVO createVmGroup(CreateVMGroupCmd cmd) { @@ -6492,7 +6497,7 @@ private void validateVolumes(List volumes) { } } - private void detachAndDeleteVolumes(List volumes) { + private void detachVolumesFromVm(List volumes) { for (VolumeVO volume : volumes) { @@ -6501,6 +6506,12 @@ private void detachAndDeleteVolumes(List volumes) { if (detachResult == null) { s_logger.error("DestroyVM remove volume - failed to detach and delete volume " + volume.getInstanceId() + " from instance " + volume.getId()); } + } + } + + private void deleteVolumesFromVm(List volumes) { + + for (VolumeVO volume : volumes) { boolean deleteResult = _volumeService.deleteVolume(volume.getId(), CallContext.current().getCallingAccount()); diff --git a/ui/scripts/instances.js b/ui/scripts/instances.js index 2073b682de33..7ec3e8d7f56e 100644 --- a/ui/scripts/instances.js +++ b/ui/scripts/instances.js @@ -169,7 +169,7 @@ } $.extend(data, { - volumeIds: $(selectedVolumes).map(function(index, volume) { + volumeids: $(selectedVolumes).map(function(index, volume) { return volume; }).toArray().join(',') }); From ff5004e417d72c4ae734cd7501a30ffd78ff8642 Mon Sep 17 00:00:00 2001 From: Boris Date: Thu, 2 Aug 2018 12:46:59 +0300 Subject: [PATCH 10/12] Adding a test for DestroyVM with volumes --- test/integration/smoke/test_vm_life_cycle.py | 93 +++----------------- 1 file changed, 11 insertions(+), 82 deletions(-) diff --git a/test/integration/smoke/test_vm_life_cycle.py b/test/integration/smoke/test_vm_life_cycle.py index d228fd8bdf60..10ef1534e435 100644 --- a/test/integration/smoke/test_vm_life_cycle.py +++ b/test/integration/smoke/test_vm_life_cycle.py @@ -33,7 +33,8 @@ Iso, Router, Configurations, - Volume) + Volume, + DiskOffering) from marvin.lib.common import (get_domain, get_zone, get_template, @@ -796,6 +797,8 @@ def test_11_destroy_vm_and_volumes(self): # 1. Deploys a VM and attaches disks to it # 2. Destroys the VM with DataDisks option + small_disk_offering = DiskOffering.list(self.apiclient, name='Small')[0] + small_virtual_machine = VirtualMachine.create( self.apiclient, self.services["small"], @@ -808,94 +811,20 @@ def test_11_destroy_vm_and_volumes(self): self.apiclient, self.services, account=self.account.name, - domainid=self.account.domainid - ) - - vol2 = Volume.create( - self.apiclient, - self.services, - account=self.account.name, - domainid=self.account.domainid + diskofferingid=small_disk_offering.id, + domainid=self.account.domainid, + zoneid=self.zone.id ) small_virtual_machine.attach_volume(self.apiclient, vol1) - small_virtual_machine.attach_volume(self.apiclient, vol2) - cmd = destroyVirtualMachine.destroyVirtualMachineCmd() - cmd.id = small_virtual_machine.id - cmd.expunge = True - cmd.volumes = vol1.id, vol2.id - - response = self.apiclient.destroyVirtualMachine(cmd) + self.debug("Destroy VM - ID: %s" % small_virtual_machine.id) + small_virtual_machine.delete(self.apiclient, volumeIds=vol1.id) - self.debug("Destroy VM - ID: %s" % self.small_virtual_machine.id) - self.small_virtual_machine.delete(self.apiclient, expunge=False) - - list_vm_response = VirtualMachine.list( - self.apiclient, - id=self.small_virtual_machine.id - ) - self.assertEqual( - isinstance(list_vm_response, list), - True, - "Check list response returns a valid list" - ) + self.assertEqual(VirtualMachine.list(self.apiclient, id=small_virtual_machine.id), None, "List response contains records when it should not") - self.assertNotEqual( - len(list_vm_response), - 0, - "Check VM avaliable in List Virtual Machines" - ) + self.assertEqual(Volume.list(self.apiclient, id=vol1.id), None, "List response contains records when it should not") - self.assertEqual( - list_vm_response[0].state, - "Destroyed", - "Check virtual machine is in destroyed state" - ) - - list_vol = Volume.list( - self.apiclient, - id=vol1.id - ) - self.assertEqual( - isinstance(list_vol, list), - True, - "Check list response returns a valid list" - ) - - self.assertNotEqual( - len(list_vol), - 0, - "Check VM avaliable in List Virtual Machines" - ) - - self.assertEqual( - list_vol[0].state, - "Destroyed", - "Check virtual machine is in destroyed state" - ) - - list_vol = Volume.list( - self.apiclient, - id=vol2.id - ) - self.assertEqual( - isinstance(list_vol, list), - True, - "Check list response returns a valid list" - ) - - self.assertNotEqual( - len(list_vol), - 0, - "Check VM avaliable in List Virtual Machines" - ) - - self.assertEqual( - list_vol[0].state, - "Destroyed", - "Check virtual machine is in destroyed state" - ) return From eb02664c4aaab44c8c22606e199da7ba1e88e295 Mon Sep 17 00:00:00 2001 From: henko holtzhausen Date: Fri, 3 Aug 2018 16:29:49 +0200 Subject: [PATCH 11/12] ui enhancement when no volumes are attached to a vm --- .../api/command/user/vm/DestroyVMCmd.java | 2 +- ui/css/cloudstack3.css | 9 +++++++++ ui/l10n/en.js | 1 + ui/scripts/instances.js | 1 + ui/scripts/ui/dialog.js | 16 ++++++++++++++-- 5 files changed, 26 insertions(+), 3 deletions(-) diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/DestroyVMCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/DestroyVMCmd.java index 15c3d3dd9435..7b359b790477 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/DestroyVMCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/DestroyVMCmd.java @@ -68,7 +68,7 @@ public class DestroyVMCmd extends BaseAsyncCmd { type = CommandType.LIST, collectionType = CommandType.UUID, entityType = VolumeResponse.class, - description = "Comma separated list of volume UUIDs that will be deleted", + description = "Comma separated list of UUIDs for volumes that will be deleted", since = "4.12.0") private List volumeIds; diff --git a/ui/css/cloudstack3.css b/ui/css/cloudstack3.css index ca85bb0bee30..148ea533a378 100644 --- a/ui/css/cloudstack3.css +++ b/ui/css/cloudstack3.css @@ -4180,6 +4180,15 @@ textarea { float: left; } +.ui-dialog div.form-container div.value label { + display: block; + width: 119px; + text-align: left; + font-size: 13px; + margin-top: 2px; + margin-left: -10px; +} + .ui-dialog div.form-container div.value input.hasDatepicker { color: #2F5D86; cursor: pointer; diff --git a/ui/l10n/en.js b/ui/l10n/en.js index 0a6d66a460f5..8d1666357e77 100644 --- a/ui/l10n/en.js +++ b/ui/l10n/en.js @@ -1808,6 +1808,7 @@ var dictionary = { "label.volgroup":"Volume Group", "label.volume":"Volume", "label.volume.details":"Volume details", +"label.volume.empty":"No volumes attached to this VM", "label.volume.ids":"Volume ID's", "label.volume.limits":"Volume Limits", "label.volume.migrated":"Volume migrated", diff --git a/ui/scripts/instances.js b/ui/scripts/instances.js index 7ec3e8d7f56e..22b029330885 100644 --- a/ui/scripts/instances.js +++ b/ui/scripts/instances.js @@ -124,6 +124,7 @@ dependsOn: 'volumes', isBoolean: true, isHidden: true, + emptyMessage: 'label.volume.empty', multiDataArray: true, multiData: function(args) { $.ajax({ diff --git a/ui/scripts/ui/dialog.js b/ui/scripts/ui/dialog.js index c22de68bfd99..3de953fd29ca 100644 --- a/ui/scripts/ui/dialog.js +++ b/ui/scripts/ui/dialog.js @@ -434,8 +434,8 @@ }); } else if (field.multiDataArray) { - $input = $('
') - .addClass('multi-array').addClass(key).appendTo($value); + + $input = $('
'); multiArgs = { context: args.context, @@ -443,7 +443,19 @@ success: function(args) { if (args.data == undefined || args.data.length == 0) { + var label = field.emptyMessage != null ? field.emptyMessage : 'No data available'; + + $input + .addClass('value') + .appendTo($value) + .append( + $('