diff --git a/api/src/main/java/com/cloud/event/EventTypes.java b/api/src/main/java/com/cloud/event/EventTypes.java index df5df0375db..dc52611c82c 100644 --- a/api/src/main/java/com/cloud/event/EventTypes.java +++ b/api/src/main/java/com/cloud/event/EventTypes.java @@ -239,6 +239,8 @@ public class EventTypes { public static final String EVENT_VOLUME_DETAIL_ADD = "VOLUME.DETAIL.ADD"; public static final String EVENT_VOLUME_DETAIL_REMOVE = "VOLUME.DETAIL.REMOVE"; public static final String EVENT_VOLUME_UPDATE = "VOLUME.UPDATE"; + public static final String EVENT_VOLUME_DESTROY = "VOLUME.DESTROY"; + public static final String EVENT_VOLUME_RECOVER = "VOLUME.RECOVER"; // Domains public static final String EVENT_DOMAIN_CREATE = "DOMAIN.CREATE"; @@ -706,6 +708,8 @@ public class EventTypes { entityEventDetails.put(EVENT_VOLUME_UPLOAD, Volume.class); entityEventDetails.put(EVENT_VOLUME_MIGRATE, Volume.class); entityEventDetails.put(EVENT_VOLUME_RESIZE, Volume.class); + entityEventDetails.put(EVENT_VOLUME_DESTROY, Volume.class); + entityEventDetails.put(EVENT_VOLUME_RECOVER, Volume.class); // Domains entityEventDetails.put(EVENT_DOMAIN_CREATE, Domain.class); diff --git a/api/src/main/java/com/cloud/storage/Volume.java b/api/src/main/java/com/cloud/storage/Volume.java index 0e86ac01638..dde9d60a848 100644 --- a/api/src/main/java/com/cloud/storage/Volume.java +++ b/api/src/main/java/com/cloud/storage/Volume.java @@ -124,6 +124,7 @@ public interface Volume extends ControlledEntity, Identity, InternalIdentity, Ba s_fsm.addTransition(new StateMachine2.Transition(Ready, Event.AttachRequested, Attaching, null)); s_fsm.addTransition(new StateMachine2.Transition(Attaching, Event.OperationSucceeded, Ready, null)); s_fsm.addTransition(new StateMachine2.Transition(Attaching, Event.OperationFailed, Ready, null)); + s_fsm.addTransition(new StateMachine2.Transition(Destroy, Event.RecoverRequested, Ready, null)); } } @@ -143,6 +144,7 @@ public interface Volume extends ControlledEntity, Identity, InternalIdentity, Ba SnapshotRequested, RevertSnapshotRequested, DestroyRequested, + RecoverRequested, ExpungingRequested, ResizeRequested, AttachRequested, diff --git a/api/src/main/java/com/cloud/storage/VolumeApiService.java b/api/src/main/java/com/cloud/storage/VolumeApiService.java index aa6d8a664c8..5c4130158cd 100644 --- a/api/src/main/java/com/cloud/storage/VolumeApiService.java +++ b/api/src/main/java/com/cloud/storage/VolumeApiService.java @@ -148,4 +148,8 @@ public interface VolumeApiService { * */ boolean doesTargetStorageSupportDiskOffering(StoragePool destPool, String diskOfferingTags); -} \ No newline at end of file + + Volume destroyVolume(long volumeId, Account caller, boolean expunge, boolean forceExpunge); + + Volume recoverVolume(long volumeId); +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/volume/DestroyVolumeCmdByAdmin.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/volume/DestroyVolumeCmdByAdmin.java new file mode 100644 index 00000000000..44ce32f90c2 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/volume/DestroyVolumeCmdByAdmin.java @@ -0,0 +1,54 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.api.command.admin.volume; + +import org.apache.log4j.Logger; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.ResponseObject.ResponseView; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.command.admin.AdminCmd; +import org.apache.cloudstack.api.command.user.volume.DestroyVolumeCmd; +import org.apache.cloudstack.api.response.VolumeResponse; +import org.apache.cloudstack.context.CallContext; + +import com.cloud.storage.Volume; + +@APICommand(name = "destroyVolume", description = "Destroys a Volume.", responseObject = VolumeResponse.class, responseView = ResponseView.Full, entityType = {Volume.class}, + since = "4.14.0", + authorized = {RoleType.Admin}, + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = true) +public class DestroyVolumeCmdByAdmin extends DestroyVolumeCmd implements AdminCmd { + + public static final Logger s_logger = Logger.getLogger(DestroyVolumeCmdByAdmin.class.getName()); + + @Override + public void execute() { + CallContext.current().setEventDetails("Volume Id: " + getId()); + Volume result = _volumeService.destroyVolume(getId(), CallContext.current().getCallingAccount(), getExpunge(), false); + if (result != null) { + VolumeResponse response = _responseGenerator.createVolumeResponse(ResponseView.Full, result); + response.setResponseName(getCommandName()); + setResponseObject(response); + } else { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to destroy volume"); + } + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/volume/RecoverVolumeCmdByAdmin.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/volume/RecoverVolumeCmdByAdmin.java new file mode 100644 index 00000000000..f51aeec9719 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/volume/RecoverVolumeCmdByAdmin.java @@ -0,0 +1,53 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.api.command.admin.volume; + +import org.apache.log4j.Logger; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.ResponseObject.ResponseView; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.command.admin.AdminCmd; +import org.apache.cloudstack.api.command.user.volume.RecoverVolumeCmd; +import org.apache.cloudstack.api.response.VolumeResponse; +import org.apache.cloudstack.context.CallContext; + +import com.cloud.storage.Volume; + +@APICommand(name = "recoverVolume", description = "Recovers a Destroy volume.", responseObject = VolumeResponse.class, responseView = ResponseView.Full, entityType = {Volume.class}, + since = "4.14.0", + authorized = {RoleType.Admin}, + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = true) +public class RecoverVolumeCmdByAdmin extends RecoverVolumeCmd implements AdminCmd { + public static final Logger s_logger = Logger.getLogger(RecoverVolumeCmdByAdmin.class.getName()); + + @Override + public void execute() { + CallContext.current().setEventDetails("Volume Id: " + getId()); + Volume result = _volumeService.recoverVolume(getId()); + if (result != null) { + VolumeResponse response = _responseGenerator.createVolumeResponse(ResponseView.Full, result); + response.setResponseName(getCommandName()); + setResponseObject(response); + } else { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to recover volume"); + } + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/config/ListCapabilitiesCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/config/ListCapabilitiesCmd.java index 40d1a71e966..3f1b9a23de3 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/config/ListCapabilitiesCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/config/ListCapabilitiesCmd.java @@ -59,6 +59,7 @@ public class ListCapabilitiesCmd extends BaseCmd { response.setKVMSnapshotEnabled((Boolean)capabilities.get("KVMSnapshotEnabled")); response.setAllowUserViewDestroyedVM((Boolean)capabilities.get("allowUserViewDestroyedVM")); response.setAllowUserExpungeRecoverVM((Boolean)capabilities.get("allowUserExpungeRecoverVM")); + response.setAllowUserExpungeRecoverVolume((Boolean)capabilities.get("allowUserExpungeRecoverVolume")); response.setAllowUserViewAllDomainAccounts((Boolean)capabilities.get("allowUserViewAllDomainAccounts")); if (capabilities.containsKey("apiLimitInterval")) { response.setApiLimitInterval((Integer)capabilities.get("apiLimitInterval")); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/volume/DeleteVolumeCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/volume/DeleteVolumeCmd.java index 070ec5fba74..678299c8b5a 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/volume/DeleteVolumeCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/volume/DeleteVolumeCmd.java @@ -82,8 +82,8 @@ public class DeleteVolumeCmd extends BaseCmd { @Override public void execute() throws ConcurrentOperationException { CallContext.current().setEventDetails("Volume Id: " + this._uuidMgr.getUuid(Volume.class, getId())); - boolean result = _volumeService.deleteVolume(id, CallContext.current().getCallingAccount()); - if (result) { + Volume result = _volumeService.destroyVolume(id, CallContext.current().getCallingAccount(), true, false); + if (result != null) { SuccessResponse response = new SuccessResponse(getCommandName()); setResponseObject(response); } else { diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/volume/DestroyVolumeCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/volume/DestroyVolumeCmd.java new file mode 100644 index 00000000000..ed845789390 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/volume/DestroyVolumeCmd.java @@ -0,0 +1,130 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.api.command.user.volume; + +import org.apache.log4j.Logger; + +import org.apache.cloudstack.acl.RoleType; +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.VolumeResponse; +import org.apache.cloudstack.context.CallContext; + +import com.cloud.event.EventTypes; +import com.cloud.storage.Volume; +import com.cloud.user.Account; + +@APICommand(name = "destroyVolume", description = "Destroys a Volume.", responseObject = VolumeResponse.class, responseView = ResponseView.Restricted, entityType = {Volume.class}, + since = "4.14.0", + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}, + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = true) +public class DestroyVolumeCmd extends BaseAsyncCmd { + public static final Logger s_logger = Logger.getLogger(DestroyVolumeCmd.class.getName()); + + private static final String s_name = "destroyvolumeresponse"; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @ACL(accessType = AccessType.OperateEntry) + @Parameter(name=ApiConstants.ID, type=CommandType.UUID, entityType=VolumeResponse.class, + required=true, description="The ID of the volume") + private Long id; + + @Parameter(name = ApiConstants.EXPUNGE, + type = CommandType.BOOLEAN, + description = "If true is passed, the volume is expunged immediately. False by default.", + since = "4.6.0") + private Boolean expunge; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public Long getId() { + return id; + } + + public boolean getExpunge() { + if (expunge == null) { + return false; + } + return expunge; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public String getCommandName() { + return s_name; + } + + @Override + public long getEntityOwnerId() { + Volume volume = _entityMgr.findById(Volume.class, getId()); + if (volume != null) { + return volume.getAccountId(); + } + + return Account.ACCOUNT_ID_SYSTEM; // no account info given, parent this command to SYSTEM so ERROR events are tracked + } + + @Override + public String getEventType() { + return EventTypes.EVENT_VOLUME_DESTROY; + } + + @Override + public String getEventDescription() { + return "destroying volume: " + getId(); + } + + @Override + public ApiCommandJobType getInstanceType() { + return ApiCommandJobType.Volume; + } + + @Override + public Long getInstanceId() { + return getId(); + } + + @Override + public void execute() { + CallContext.current().setEventDetails("Volume Id: " + getId()); + Volume result = _volumeService.destroyVolume(getId(), CallContext.current().getCallingAccount(), getExpunge(), false); + if (result != null) { + VolumeResponse response = _responseGenerator.createVolumeResponse(ResponseView.Restricted, result); + response.setResponseName(getCommandName()); + setResponseObject(response); + } else { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to destroy volume"); + } + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/volume/ListVolumesCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/volume/ListVolumesCmd.java index aa40a24946e..0b3e6dd5ce1 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/volume/ListVolumesCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/volume/ListVolumesCmd.java @@ -24,6 +24,7 @@ import org.apache.cloudstack.api.ApiCommandJobType; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.BaseListTaggedResourcesCmd; import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.BaseCmd.CommandType; import org.apache.cloudstack.api.ResponseObject.ResponseView; import org.apache.cloudstack.api.command.user.UserCmd; import org.apache.cloudstack.api.response.ClusterResponse; @@ -88,6 +89,9 @@ public class ListVolumesCmd extends BaseListTaggedResourcesCmd implements UserCm RoleType.Admin}) private Boolean display; + @Parameter(name = ApiConstants.STATE, type = CommandType.STRING, description = "state of the volume. Possible values are: Ready, Allocated, Destroy, Expunging, Expunged.") + private String state; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -139,6 +143,10 @@ public class ListVolumesCmd extends BaseListTaggedResourcesCmd implements UserCm } return super.getDisplay(); } + + public String getState() { + return state; + } ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/volume/RecoverVolumeCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/volume/RecoverVolumeCmd.java new file mode 100644 index 00000000000..f5bb1ddd676 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/volume/RecoverVolumeCmd.java @@ -0,0 +1,91 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.api.command.user.volume; + +import org.apache.log4j.Logger; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseCmd; +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.VolumeResponse; +import org.apache.cloudstack.context.CallContext; + +import com.cloud.storage.Volume; +import com.cloud.user.Account; + +@APICommand(name = "recoverVolume", description = "Recovers a Destroy volume.", responseObject = VolumeResponse.class, responseView = ResponseView.Restricted, entityType = {Volume.class}, + since = "4.14.0", + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}, + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = true) +public class RecoverVolumeCmd extends BaseCmd { + public static final Logger s_logger = Logger.getLogger(RecoverVolumeCmd.class.getName()); + + private static final String s_name = "recovervolumeresponse"; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = VolumeResponse.class, required = true, description = "The ID of the volume") + private Long id; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public Long getId() { + return id; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public String getCommandName() { + return s_name; + } + + @Override + public long getEntityOwnerId() { + Volume volume = _entityMgr.findById(Volume.class, getId()); + if (volume != null) { + return volume.getAccountId(); + } + + return Account.ACCOUNT_ID_SYSTEM; // no account info given, parent this command to SYSTEM so ERROR events are tracked + } + + @Override + public void execute() { + CallContext.current().setEventDetails("Volume Id: " + getId()); + Volume result = _volumeService.recoverVolume(getId()); + if (result != null) { + VolumeResponse response = _responseGenerator.createVolumeResponse(ResponseView.Full, result); + response.setResponseName(getCommandName()); + setResponseObject(response); + } else { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to recover volume"); + } + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/response/CapabilitiesResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/CapabilitiesResponse.java index 153d7dfca9a..352f559125f 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/CapabilitiesResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/CapabilitiesResponse.java @@ -84,6 +84,10 @@ public class CapabilitiesResponse extends BaseResponse { @Param(description = "true if the user can recover and expunge virtualmachines, false otherwise", since = "4.6.0") private boolean allowUserExpungeRecoverVM; + @SerializedName("allowuserexpungerecovervolume") + @Param(description = "true if the user can recover and expunge volumes, false otherwise", since = "4.14.0") + private boolean allowUserExpungeRecoverVolume; + @SerializedName("allowuserviewalldomainaccounts") @Param(description = "true if users can see all accounts within the same domain, false otherwise") private boolean allowUserViewAllDomainAccounts; @@ -148,7 +152,11 @@ public class CapabilitiesResponse extends BaseResponse { this.allowUserExpungeRecoverVM = allowUserExpungeRecoverVM; } + public void setAllowUserExpungeRecoverVolume(boolean allowUserExpungeRecoverVolume) { + this.allowUserExpungeRecoverVolume = allowUserExpungeRecoverVolume; + } + public void setAllowUserViewAllDomainAccounts(boolean allowUserViewAllDomainAccounts) { this.allowUserViewAllDomainAccounts = allowUserViewAllDomainAccounts; } -} \ No newline at end of file +} diff --git a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java index 95538c2047f..9e2168e0bfd 100644 --- a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java +++ b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java @@ -1578,6 +1578,8 @@ public class VolumeOrchestrator extends ManagerBase implements VolumeOrchestrati if (volume.getState() == Volume.State.Allocated) { _volsDao.remove(volume.getId()); stateTransitTo(volume, Volume.Event.DestroyRequested); + _resourceLimitMgr.decrementResourceCount(volume.getAccountId(), ResourceType.volume, volume.isDisplay()); + _resourceLimitMgr.decrementResourceCount(volume.getAccountId(), ResourceType.primary_storage, volume.isDisplay(), new Long(volume.getSize())); } else { volService.destroyVolume(volume.getId()); } @@ -1585,8 +1587,6 @@ public class VolumeOrchestrator extends ManagerBase implements VolumeOrchestrati // publish usage event for the volume UsageEventUtils.publishUsageEvent(EventTypes.EVENT_VOLUME_DELETE, volume.getAccountId(), volume.getDataCenterId(), volume.getId(), volume.getName(), Volume.class.getName(), volume.getUuid(), volume.isDisplayVolume()); - _resourceLimitMgr.decrementResourceCount(volume.getAccountId(), ResourceType.volume, volume.isDisplay()); - _resourceLimitMgr.decrementResourceCount(volume.getAccountId(), ResourceType.primary_storage, volume.isDisplay(), new Long(volume.getSize())); } catch (Exception e) { s_logger.debug("Failed to destroy volume" + volume.getId(), e); throw new CloudRuntimeException("Failed to destroy volume" + volume.getId(), e); diff --git a/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/volume/VolumeServiceImpl.java b/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/volume/VolumeServiceImpl.java index 2a642f02673..92c8a93f515 100644 --- a/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/volume/VolumeServiceImpl.java +++ b/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/volume/VolumeServiceImpl.java @@ -119,6 +119,7 @@ import com.cloud.utils.Pair; import com.cloud.utils.db.DB; import com.cloud.utils.db.GlobalLock; import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.vm.VirtualMachine; @Component public class VolumeServiceImpl implements VolumeService { @@ -325,7 +326,10 @@ public class VolumeServiceImpl implements VolumeService { VolumeDataStoreVO volumeStore = _volumeStoreDao.findByVolume(volume.getId()); if (volumeStore != null) { if (volumeStore.getDownloadState() == VMTemplateStorageResourceAssoc.Status.DOWNLOAD_IN_PROGRESS) { - s_logger.debug("Volume: " + volume.getName() + " is currently being uploaded; cant' delete it."); + String msg = "Volume: " + volume.getName() + " is currently being uploaded; cant' delete it."; + s_logger.debug(msg); + result.setSuccess(false); + result.setResult(msg); future.complete(result); return future; } @@ -1213,6 +1217,12 @@ public class VolumeServiceImpl implements VolumeService { snapshotMgr.deletePoliciesForVolume(volumeId); vol.stateTransit(Volume.Event.OperationSucceeded); + + if (vol.getAttachedVM() == null || vol.getAttachedVM().getType() == VirtualMachine.Type.User) { + // Decrement the resource count for volumes and primary storage belonging user VM's only + _resourceLimitMgr.decrementResourceCount(vol.getAccountId(), ResourceType.volume, vol.isDisplay()); + _resourceLimitMgr.decrementResourceCount(vol.getAccountId(), ResourceType.primary_storage, vol.isDisplay(), new Long(vol.getSize())); + } } @Override diff --git a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java index 6f90d0825da..fcd50e56bbc 100644 --- a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java +++ b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java @@ -1809,6 +1809,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q Long clusterId = cmd.getClusterId(); Long diskOffId = cmd.getDiskOfferingId(); Boolean display = cmd.getDisplay(); + String state = cmd.getState(); Long zoneId = cmd.getZoneId(); Long podId = cmd.getPodId(); @@ -1844,8 +1845,8 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q sb.and("storageId", sb.entity().getPoolUuid(), SearchCriteria.Op.EQ); sb.and("diskOfferingId", sb.entity().getDiskOfferingId(), SearchCriteria.Op.EQ); sb.and("display", sb.entity().isDisplayVolume(), SearchCriteria.Op.EQ); - // Only return volumes that are not destroyed - sb.and("state", sb.entity().getState(), SearchCriteria.Op.NEQ); + sb.and("state", sb.entity().getState(), SearchCriteria.Op.EQ); + sb.and("stateNEQ", sb.entity().getState(), SearchCriteria.Op.NEQ); sb.and("systemUse", sb.entity().isSystemUse(), SearchCriteria.Op.NEQ); // display UserVM volumes only sb.and().op("type", sb.entity().getVmType(), SearchCriteria.Op.NIN); @@ -1860,6 +1861,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q SearchCriteria ssc = _volumeJoinDao.createSearchCriteria(); ssc.addOr("name", SearchCriteria.Op.LIKE, "%" + keyword + "%"); ssc.addOr("volumeType", SearchCriteria.Op.LIKE, "%" + keyword + "%"); + ssc.addOr("state", SearchCriteria.Op.LIKE, "%" + keyword + "%"); sc.addAnd("name", SearchCriteria.Op.SC, ssc); } @@ -1918,8 +1920,11 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q // Don't return DomR and ConsoleProxy volumes sc.setParameters("type", VirtualMachine.Type.ConsoleProxy, VirtualMachine.Type.SecondaryStorageVm, VirtualMachine.Type.DomainRouter); - // Only return volumes that are not destroyed - sc.setParameters("state", Volume.State.Destroy); + if (state != null) { + sc.setParameters("state", state); + } else if (!_accountMgr.isAdmin(caller.getId())) { + sc.setParameters("stateNEQ", Volume.State.Expunged); + } // search Volume details by ids Pair, Integer> uniqueVolPair = _volumeJoinDao.searchAndCount(sc, searchFilter); diff --git a/server/src/main/java/com/cloud/server/ManagementServerImpl.java b/server/src/main/java/com/cloud/server/ManagementServerImpl.java index f904256b321..a68b3b80d0c 100644 --- a/server/src/main/java/com/cloud/server/ManagementServerImpl.java +++ b/server/src/main/java/com/cloud/server/ManagementServerImpl.java @@ -265,9 +265,11 @@ import org.apache.cloudstack.api.command.admin.vm.UpgradeVMCmdByAdmin; import org.apache.cloudstack.api.command.admin.vmsnapshot.RevertToVMSnapshotCmdByAdmin; import org.apache.cloudstack.api.command.admin.volume.AttachVolumeCmdByAdmin; import org.apache.cloudstack.api.command.admin.volume.CreateVolumeCmdByAdmin; +import org.apache.cloudstack.api.command.admin.volume.DestroyVolumeCmdByAdmin; import org.apache.cloudstack.api.command.admin.volume.DetachVolumeCmdByAdmin; import org.apache.cloudstack.api.command.admin.volume.ListVolumesCmdByAdmin; import org.apache.cloudstack.api.command.admin.volume.MigrateVolumeCmdByAdmin; +import org.apache.cloudstack.api.command.admin.volume.RecoverVolumeCmdByAdmin; import org.apache.cloudstack.api.command.admin.volume.ResizeVolumeCmdByAdmin; import org.apache.cloudstack.api.command.admin.volume.UpdateVolumeCmdByAdmin; import org.apache.cloudstack.api.command.admin.volume.UploadVolumeCmdByAdmin; @@ -483,12 +485,14 @@ import org.apache.cloudstack.api.command.user.volume.AddResourceDetailCmd; 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.DeleteVolumeCmd; +import org.apache.cloudstack.api.command.user.volume.DestroyVolumeCmd; 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.ListResourceDetailsCmd; import org.apache.cloudstack.api.command.user.volume.ListVolumesCmd; import org.apache.cloudstack.api.command.user.volume.MigrateVolumeCmd; +import org.apache.cloudstack.api.command.user.volume.RecoverVolumeCmd; import org.apache.cloudstack.api.command.user.volume.RemoveResourceDetailCmd; import org.apache.cloudstack.api.command.user.volume.ResizeVolumeCmd; import org.apache.cloudstack.api.command.user.volume.UpdateVolumeCmd; @@ -643,6 +647,7 @@ import com.cloud.storage.ScopeType; import com.cloud.storage.StorageManager; import com.cloud.storage.StoragePool; import com.cloud.storage.Volume; +import com.cloud.storage.VolumeApiServiceImpl; import com.cloud.storage.VolumeVO; import com.cloud.storage.dao.DiskOfferingDao; import com.cloud.storage.dao.GuestOSCategoryDao; @@ -2948,6 +2953,8 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe cmdList.add(MigrateVolumeCmd.class); cmdList.add(ResizeVolumeCmd.class); cmdList.add(UploadVolumeCmd.class); + cmdList.add(DestroyVolumeCmd.class); + cmdList.add(RecoverVolumeCmd.class); cmdList.add(CreateStaticRouteCmd.class); cmdList.add(CreateVPCCmd.class); cmdList.add(DeleteStaticRouteCmd.class); @@ -3093,6 +3100,8 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe cmdList.add(UpdateVolumeCmdByAdmin.class); cmdList.add(UploadVolumeCmdByAdmin.class); cmdList.add(ListVolumesCmdByAdmin.class); + cmdList.add(DestroyVolumeCmdByAdmin.class); + cmdList.add(RecoverVolumeCmdByAdmin.class); cmdList.add(AssociateIPAddrCmdByAdmin.class); cmdList.add(ListPublicIpAddressesCmdByAdmin.class); cmdList.add(CreateNetworkCmdByAdmin.class); @@ -3501,6 +3510,7 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe final boolean allowUserViewDestroyedVM = (QueryService.AllowUserViewDestroyedVM.valueIn(caller.getId()) | _accountService.isAdmin(caller.getId())); final boolean allowUserExpungeRecoverVM = (UserVmManager.AllowUserExpungeRecoverVm.valueIn(caller.getId()) | _accountService.isAdmin(caller.getId())); + final boolean allowUserExpungeRecoverVolume = (VolumeApiServiceImpl.AllowUserExpungeRecoverVolume.valueIn(caller.getId()) | _accountService.isAdmin(caller.getId())); final boolean allowUserViewAllDomainAccounts = (QueryService.AllowUserViewAllDomainAccounts.valueIn(caller.getDomainId())); @@ -3523,6 +3533,7 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe capabilities.put("KVMSnapshotEnabled", KVMSnapshotEnabled); capabilities.put("allowUserViewDestroyedVM", allowUserViewDestroyedVM); capabilities.put("allowUserExpungeRecoverVM", allowUserExpungeRecoverVM); + capabilities.put("allowUserExpungeRecoverVolume", allowUserExpungeRecoverVolume); capabilities.put("allowUserViewAllDomainAccounts", allowUserViewAllDomainAccounts); if (apiLimitEnabled) { capabilities.put("apiLimitInterval", apiLimitInterval); diff --git a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java index e6022513508..15e32208b0f 100644 --- a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java +++ b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java @@ -275,6 +275,9 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic static final ConfigKey VolumeUrlCheck = new ConfigKey("Advanced", Boolean.class, "volume.url.check", "true", "Check the url for a volume before downloading it from the management server. Set to false when you managment has no internet access.", true); + public static final ConfigKey AllowUserExpungeRecoverVolume = new ConfigKey("Advanced", Boolean.class, "allow.user.expunge.recover.volume", "true", + "Determines whether users can expunge or recover their volume", true, ConfigKey.Scope.Account); + private long _maxVolumeSizeInGb; private final StateMachine2 _volStateMachine; @@ -1261,20 +1264,17 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic * Otherwise, after the removal in the database, we will try to remove the volume from both primary and secondary storage. */ public boolean deleteVolume(long volumeId, Account caller) throws ConcurrentOperationException { - VolumeVO volume = retrieveAndValidateVolume(volumeId, caller); + Volume volume = destroyVolume(volumeId, caller, true, true); + return (volume != null); + } + + private boolean deleteVolumeFromStorage(VolumeVO volume, Account caller) throws ConcurrentOperationException { try { - destroyVolumeIfPossible(volume); - // Mark volume as removed if volume has not been created on primary or secondary - if (volume.getState() == Volume.State.Allocated) { - _volsDao.remove(volumeId); - stateTransitTo(volume, Volume.Event.DestroyRequested); - return true; - } expungeVolumesInPrimaryStorageIfNeeded(volume); expungeVolumesInSecondaryStorageIfNeeded(volume); cleanVolumesCache(volume); return true; - } catch (InterruptedException | ExecutionException | NoTransitionException e) { + } catch (InterruptedException | ExecutionException e) { s_logger.warn("Failed to expunge volume: " + volume.getUuid(), e); return false; } @@ -1301,7 +1301,7 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic if (!_snapshotMgr.canOperateOnVolume(volume)) { throw new InvalidParameterValueException("There are snapshot operations in progress on the volume, unable to delete it"); } - if (volume.getInstanceId() != null) { + if (volume.getInstanceId() != null && volume.getState() != Volume.State.Expunged) { throw new InvalidParameterValueException("Please specify a volume that is not attached to any VM."); } if (volume.getState() == Volume.State.UploadOp) { @@ -1334,12 +1334,8 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic * The volume is destroyed via {@link VolumeService#destroyVolume(long)} method. */ protected void destroyVolumeIfPossible(VolumeVO volume) { - if (volume.getState() != Volume.State.Destroy && volume.getState() != Volume.State.Expunging && volume.getState() != Volume.State.Expunged) { + if (volume.getState() != Volume.State.Destroy && volume.getState() != Volume.State.Expunging && volume.getState() != Volume.State.Expunged && volume.getState() != Volume.State.Allocated && volume.getState() != Volume.State.Uploaded) { volService.destroyVolume(volume.getId()); - - // Decrement the resource count for volumes and primary storage belonging user VM's only - _resourceLimitMgr.decrementResourceCount(volume.getAccountId(), ResourceType.volume, volume.isDisplayVolume()); - _resourceLimitMgr.decrementResourceCount(volume.getAccountId(), ResourceType.primary_storage, volume.isDisplayVolume(), volume.getSize()); } } @@ -1389,6 +1385,89 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic return _volStateMachine.transitTo(vol, event, null, _volsDao); } + @Override + @ActionEvent(eventType = EventTypes.EVENT_VOLUME_DESTROY, eventDescription = "destroying a volume") + public Volume destroyVolume(long volumeId, Account caller, boolean expunge, boolean forceExpunge) { + VolumeVO volume = retrieveAndValidateVolume(volumeId, caller); + + if (expunge) { + // When trying to expunge, permission is denied when the caller is not an admin and the AllowUserExpungeRecoverVolume is false for the caller. + final Long userId = caller.getAccountId(); + if (!forceExpunge && !_accountMgr.isAdmin(userId) && !AllowUserExpungeRecoverVolume.valueIn(userId)) { + throw new PermissionDeniedException("Expunging a volume can only be done by an Admin. Or when the allow.user.expunge.recover.volume key is set."); + } + } else if (volume.getState() == Volume.State.Allocated || volume.getState() == Volume.State.Uploaded) { + throw new InvalidParameterValueException("The volume in Allocated/Uploaded state can only be expunged not destroyed/recovered"); + } + + destroyVolumeIfPossible(volume); + + if (expunge) { + // Mark volume as removed if volume has not been created on primary or secondary + if (volume.getState() == Volume.State.Allocated) { + _volsDao.remove(volume.getId()); + try { + stateTransitTo(volume, Volume.Event.DestroyRequested); + } catch (NoTransitionException e) { + s_logger.debug("Failed to destroy volume" + volume.getId(), e); + return null; + } + _resourceLimitMgr.decrementResourceCount(volume.getAccountId(), ResourceType.volume, volume.isDisplay()); + _resourceLimitMgr.decrementResourceCount(volume.getAccountId(), ResourceType.primary_storage, volume.isDisplay(), new Long(volume.getSize())); + return volume; + } + if (!deleteVolumeFromStorage(volume, caller)) { + s_logger.warn("Failed to expunge volume: " + volumeId); + return null; + } + } + + return volume; + } + + @Override + @ActionEvent(eventType = EventTypes.EVENT_VOLUME_RECOVER, eventDescription = "recovering a volume in Destroy state") + public Volume recoverVolume(long volumeId) { + Account caller = CallContext.current().getCallingAccount(); + final Long userId = caller.getAccountId(); + + // Verify input parameters + final VolumeVO volume = _volsDao.findById(volumeId); + + if (volume == null) { + throw new InvalidParameterValueException("Unable to find a volume with id " + volume); + } + + // When trying to expunge, permission is denied when the caller is not an admin and the AllowUserExpungeRecoverVolume is false for the caller. + if (!_accountMgr.isAdmin(userId) && !AllowUserExpungeRecoverVolume.valueIn(userId)) { + throw new PermissionDeniedException("Recovering a volume can only be done by an Admin. Or when the allow.user.expunge.recover.volume key is set."); + } + + _accountMgr.checkAccess(caller, null, true, volume); + + if (volume.getState() != Volume.State.Destroy) { + throw new InvalidParameterValueException("Please specify a volume in Destroy state."); + } + + try { + _resourceLimitMgr.checkResourceLimit(_accountMgr.getAccount(volume.getAccountId()), ResourceType.primary_storage, volume.isDisplayVolume(), volume.getSize()); + } catch (ResourceAllocationException e) { + s_logger.error("primary storage resource limit check failed", e); + throw new InvalidParameterValueException(e.getMessage()); + } + + try { + stateTransitTo(volume, Volume.Event.RecoverRequested); + } catch (NoTransitionException e) { + s_logger.debug("Failed to recover volume" + volume.getId(), e); + throw new CloudRuntimeException("Failed to recover volume" + volume.getId(), e); + } + _resourceLimitMgr.incrementResourceCount(volume.getAccountId(), ResourceType.volume, volume.isDisplay()); + _resourceLimitMgr.incrementResourceCount(volume.getAccountId(), ResourceType.primary_storage, volume.isDisplay(), new Long(volume.getSize())); + + return volume; + } + @Override @ActionEvent(eventType = EventTypes.EVENT_VOLUME_ATTACH, eventDescription = "attaching volume", async = true) public Volume attachVolumeToVM(AttachVolumeCmd command) { @@ -3409,6 +3488,6 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic @Override public ConfigKey[] getConfigKeys() { - return new ConfigKey[] {ConcurrentMigrationsThresholdPerDatastore}; + return new ConfigKey[] {ConcurrentMigrationsThresholdPerDatastore, AllowUserExpungeRecoverVolume}; } } diff --git a/server/src/main/java/com/cloud/user/AccountManagerImpl.java b/server/src/main/java/com/cloud/user/AccountManagerImpl.java index 6ef30ebc69f..f40b888808a 100644 --- a/server/src/main/java/com/cloud/user/AccountManagerImpl.java +++ b/server/src/main/java/com/cloud/user/AccountManagerImpl.java @@ -132,7 +132,6 @@ import com.cloud.region.ha.GlobalLoadBalancingRulesService; import com.cloud.server.auth.UserAuthenticator; import com.cloud.server.auth.UserAuthenticator.ActionOnFailedAuthentication; import com.cloud.storage.VMTemplateVO; -import com.cloud.storage.Volume; import com.cloud.storage.VolumeApiService; import com.cloud.storage.VolumeVO; import com.cloud.storage.dao.VMTemplateDao; @@ -782,13 +781,11 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M // Mark the account's volumes as destroyed List volumes = _volumeDao.findDetachedByAccount(accountId); for (VolumeVO volume : volumes) { - if (!volume.getState().equals(Volume.State.Destroy)) { - try { - volumeService.deleteVolume(volume.getId(), caller); - } catch (Exception ex) { - s_logger.warn("Failed to cleanup volumes as a part of account id=" + accountId + " cleanup due to Exception: ", ex); - accountCleanupNeeded = true; - } + try { + volumeService.deleteVolume(volume.getId(), caller); + } catch (Exception ex) { + s_logger.warn("Failed to cleanup volumes as a part of account id=" + accountId + " cleanup due to Exception: ", ex); + accountCleanupNeeded = true; } } diff --git a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java index 5726377d24e..de204562bd8 100644 --- a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java +++ b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java @@ -2234,11 +2234,6 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir List rootVol = _volsDao.findByInstanceAndType(vm.getId(), Volume.Type.ROOT); // expunge the vm _itMgr.advanceExpunge(vm.getUuid()); - // Update Resource count - if (vm.getAccountId() != Account.ACCOUNT_ID_SYSTEM && !rootVol.isEmpty()) { - _resourceLimitMgr.decrementResourceCount(vm.getAccountId(), ResourceType.volume); - _resourceLimitMgr.decrementResourceCount(vm.getAccountId(), ResourceType.primary_storage, new Long(rootVol.get(0).getSize())); - } // Only if vm is not expunged already, cleanup it's resources if (vm.getRemoved() == null) { diff --git a/server/src/test/java/com/cloud/storage/VolumeApiServiceImplTest.java b/server/src/test/java/com/cloud/storage/VolumeApiServiceImplTest.java index da346536d5a..da73a2f4db3 100644 --- a/server/src/test/java/com/cloud/storage/VolumeApiServiceImplTest.java +++ b/server/src/test/java/com/cloud/storage/VolumeApiServiceImplTest.java @@ -735,8 +735,6 @@ public class VolumeApiServiceImplTest { volumeApiServiceImpl.destroyVolumeIfPossible(volumeVoMock); Mockito.verify(volumeServiceMock, Mockito.times(1)).destroyVolume(volumeMockId); - Mockito.verify(resourceLimitServiceMock, Mockito.times(1)).decrementResourceCount(accountMockId, ResourceType.volume, true); - Mockito.verify(resourceLimitServiceMock, Mockito.times(1)).decrementResourceCount(accountMockId, ResourceType.primary_storage, true, volumeSizeMock); } private void verifyMocksForTestDestroyVolumeWhenVolumeIsNotInRightState() { @@ -969,26 +967,6 @@ public class VolumeApiServiceImplTest { Mockito.verify(volumeApiServiceImpl, Mockito.times(0)).stateTransitTo(volumeVoMock, Volume.Event.DestroyRequested); } - @Test - public void deleteVolumeTestVolumeStateReadyThrowingNoTransitionException() throws InterruptedException, ExecutionException, NoTransitionException { - Mockito.doReturn(Volume.State.Ready).when(volumeVoMock).getState(); - - Mockito.doReturn(volumeVoMock).when(volumeApiServiceImpl).retrieveAndValidateVolume(volumeMockId, accountMock); - Mockito.doNothing().when(volumeApiServiceImpl).destroyVolumeIfPossible(volumeVoMock); - Mockito.doThrow(NoTransitionException.class).when(volumeApiServiceImpl).expungeVolumesInPrimaryStorageIfNeeded(volumeVoMock); - - Mockito.doReturn(true).when(volumeDaoMock).remove(volumeMockId); - Mockito.doReturn(true).when(volumeApiServiceImpl).stateTransitTo(volumeVoMock, Volume.Event.DestroyRequested); - - boolean result = volumeApiServiceImpl.deleteVolume(volumeMockId, accountMock); - - Assert.assertFalse(result); - Mockito.verify(volumeApiServiceImpl).retrieveAndValidateVolume(volumeMockId, accountMock); - Mockito.verify(volumeApiServiceImpl).destroyVolumeIfPossible(volumeVoMock); - Mockito.verify(volumeDaoMock, Mockito.times(0)).remove(volumeMockId); - Mockito.verify(volumeApiServiceImpl, Mockito.times(0)).stateTransitTo(volumeVoMock, Volume.Event.DestroyRequested); - } - @Test(expected = RuntimeException.class) public void deleteVolumeTestVolumeStateReadyThrowingRuntimeException() throws InterruptedException, ExecutionException, NoTransitionException { Mockito.doReturn(Volume.State.Ready).when(volumeVoMock).getState(); diff --git a/test/integration/component/test_volume_destroy_recover.py b/test/integration/component/test_volume_destroy_recover.py new file mode 100644 index 00000000000..c9e11c08393 --- /dev/null +++ b/test/integration/component/test_volume_destroy_recover.py @@ -0,0 +1,507 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +""" tests for Volume improvement (Destroy/Recover) in cloudstack 4.14.0.0 + +""" +# Import Local Modules +from nose.plugins.attrib import attr +from marvin.cloudstackTestCase import cloudstackTestCase +from marvin.cloudstackAPI import (deleteVolume, extractVolume, recoverVolume) +from marvin.lib.utils import (validateList, + cleanup_resources) +from marvin.lib.base import (Resources, + Volume, + Account, + Domain, + Network, + NetworkOffering, + VirtualMachine, + ServiceOffering, + DiskOffering, + Zone) +from marvin.lib.common import (get_domain, + get_zone, + get_template, + matchResourceCount, + isAccountResourceCountEqualToExpectedCount) +from marvin.codes import (PASS, FAILED, RESOURCE_PRIMARY_STORAGE, RESOURCE_VOLUME) +import logging +import random +import time + +class TestVolumeDestroyRecover(cloudstackTestCase): + @classmethod + def setUpClass(cls): + cls.testClient = super( + TestVolumeDestroyRecover, + cls).getClsTestClient() + cls.apiclient = cls.testClient.getApiClient() + cls.services = cls.testClient.getParsedTestDataConfig() + zone = get_zone(cls.apiclient, cls.testClient.getZoneForTests()) + cls.zone = Zone(zone.__dict__) + cls._cleanup = [] + + cls.logger = logging.getLogger("TestVolumeDestroyRecover") + cls.stream_handler = logging.StreamHandler() + cls.logger.setLevel(logging.DEBUG) + cls.logger.addHandler(cls.stream_handler) + + # Get Domain and templates + cls.domain = get_domain(cls.apiclient) + + cls.template = get_template(cls.apiclient, cls.zone.id, hypervisor="KVM") + if cls.template == FAILED: + sys.exit(1) + cls.templatesize = (cls.template.size / (1024 ** 3)) + + cls.services['mode'] = cls.zone.networktype + # Create Account + cls.account = Account.create( + cls.apiclient, + cls.services["account"], + admin=True, + domainid=cls.domain.id + ) + accounts = Account.list(cls.apiclient, id=cls.account.id) + cls.expectedCount = int(accounts[0].primarystoragetotal) + cls.volumeTotal = int(accounts[0].volumetotal) + + if cls.zone.securitygroupsenabled: + cls.services["shared_network_offering"]["specifyVlan"] = 'True' + cls.services["shared_network_offering"]["specifyIpRanges"] = 'True' + + cls.network_offering = NetworkOffering.create( + cls.apiclient, + cls.services["shared_network_offering"] + ) + cls.network_offering.update(cls.apiclient, state='Enabled') + + cls.account_network = Network.create( + cls.apiclient, + cls.services["network2"], + networkofferingid=cls.network_offering.id, + zoneid=cls.zone.id, + accountid=cls.account.name, + domainid=cls.account.domainid + ) + else: + cls.network_offering = NetworkOffering.create( + cls.apiclient, + cls.services["isolated_network_offering"], + ) + # Enable Network offering + cls.network_offering.update(cls.apiclient, state='Enabled') + + # Create account network + cls.services["network"]["zoneid"] = cls.zone.id + cls.services["network"]["networkoffering"] = cls.network_offering.id + cls.account_network = Network.create( + cls.apiclient, + cls.services["network"], + cls.account.name, + cls.account.domainid + ) + + # Create small service offering + cls.service_offering = ServiceOffering.create( + cls.apiclient, + cls.services["service_offerings"]["small"] + ) + + # Create disk offering + cls.disk_offering = DiskOffering.create( + cls.apiclient, + cls.services["disk_offering"], + ) + + cls._cleanup.append(cls.disk_offering) + cls._cleanup.append(cls.service_offering) + cls._cleanup.append(cls.account); + cls._cleanup.append(cls.network_offering) + + @classmethod + def tearDownClass(self): + try: + cleanup_resources(self.apiclient, self._cleanup) + except Exception as e: + raise Exception("Warning: Exception during cleanup : %s" % e) + return + + def setUp(self): + self.apiclient = self.testClient.getApiClient() + self.cleanup = [] + return + + def tearDown(self): + try: + cleanup_resources(self.apiclient, self.cleanup) + except Exception as e: + raise Exception("Warning: Exception during cleanup : %s" % e) + return + + def verify_resource_count_primary_storage(self, expectedCount, volumeTotal): + response = matchResourceCount( + self.apiclient, expectedCount, + RESOURCE_PRIMARY_STORAGE, + accountid=self.account.id) + self.assertEqual(response[0], PASS, response[1]) + + result = isAccountResourceCountEqualToExpectedCount( + self.apiclient, self.account.domainid, self.account.name, + expectedCount, RESOURCE_PRIMARY_STORAGE) + self.assertFalse(result[0], result[1]) + self.assertTrue(result[2], "Resource count of primary storage does not match") + + response = matchResourceCount( + self.apiclient, volumeTotal, + RESOURCE_VOLUME, + accountid=self.account.id) + self.assertEqual(response[0], PASS, response[1]) + + result = isAccountResourceCountEqualToExpectedCount( + self.apiclient, self.account.domainid, self.account.name, + volumeTotal, RESOURCE_VOLUME) + self.assertFalse(result[0], result[1]) + self.assertTrue(result[2], "Resource count of volume does not match") + + @attr(tags=["advanced", "advancedsg"], required_hardware="false") + def test_01_create_vm_with_data_disk(self): + """Create VM with DATA disk, then destroy it (expunge=False) and expunge it + + Steps: + # 1. create vm with root disk and data disk + # 2. destroy vm, resource count of primary storage is not changed + # 3. expunge vm, resource count of primary storage decreased with size of root disk. + # 4. delete volume (data disk), resource count of primary storage decreased with size of data disk + """ + + try: + virtual_machine_1 = VirtualMachine.create( + self.apiclient, + self.services["virtual_machine"], + accountid=self.account.name, + domainid=self.account.domainid, + serviceofferingid=self.service_offering.id, + diskofferingid=self.disk_offering.id, + templateid=self.template.id, + zoneid=self.zone.id + ) + except Exception as e: + self.fail("Exception while deploying virtual machine: %s" % e) + + self.expectedCount = self.expectedCount + self.templatesize + self.disk_offering.disksize + self.volumeTotal = self.volumeTotal + 2 + self.verify_resource_count_primary_storage(self.expectedCount, self.volumeTotal); + + root_volumes_list = Volume.list( + self.apiclient, + virtualmachineid=virtual_machine_1.id, + type='ROOT', + listall=True + ) + status = validateList(root_volumes_list) + self.assertEqual(status[0], PASS, "ROOT Volume List Validation Failed") + root_volume_id = root_volumes_list[0].id + + data_volumes_list = Volume.list( + self.apiclient, + virtualmachineid=virtual_machine_1.id, + type='DATADISK', + listall=True + ) + status = validateList(data_volumes_list) + self.assertEqual(status[0], PASS, "DATADISK Volume List Validation Failed") + data_volume_id = data_volumes_list[0].id + + # destroy vm + virtual_machine_1.delete(self.apiclient, expunge=False) + self.verify_resource_count_primary_storage(self.expectedCount, self.volumeTotal) + + # expunge vm + virtual_machine_1.expunge(self.apiclient) + self.expectedCount = self.expectedCount - self.templatesize + self.volumeTotal = self.volumeTotal - 1 + self.verify_resource_count_primary_storage(self.expectedCount, self.volumeTotal) + + # delete datadisk + cmd = deleteVolume.deleteVolumeCmd() + cmd.id = data_volume_id + self.apiclient.deleteVolume(cmd) + self.expectedCount = self.expectedCount - self.disk_offering.disksize + self.volumeTotal = self.volumeTotal - 1 + self.verify_resource_count_primary_storage(self.expectedCount, self.volumeTotal) + + @attr(tags=["advanced", "advancedsg"], required_hardware="false") + def test_02_destroy_allocated_volume(self): + """Create volume, destroy it when expunge=false and expunge=true + + Steps: + # 1. create volume, resource count increases. + # 2. destroy volume (expunge = false), Exception happened. resource count no changes + # 3. destroy volume (expunge = True), resource count of primary storage decreased with size of volume. + """ + + # Create volume + volume = Volume.create( + self.apiclient, self.services["volume"], + zoneid=self.zone.id, account=self.account.name, + domainid=self.account.domainid, diskofferingid=self.disk_offering.id + ) + self.expectedCount = self.expectedCount + self.disk_offering.disksize + self.volumeTotal = self.volumeTotal + 1 + self.verify_resource_count_primary_storage(self.expectedCount, self.volumeTotal); + + # Destroy volume (expunge=False) + with self.assertRaises(Exception): + volume.destroy(self.apiclient) + + # Destroy volume (expunge=True) + volume.destroy(self.apiclient, expunge=True) + + self.expectedCount = self.expectedCount - self.disk_offering.disksize + self.volumeTotal = self.volumeTotal - 1 + self.verify_resource_count_primary_storage(self.expectedCount, self.volumeTotal); + + @attr(tags=["advanced", "advancedsg"], required_hardware="false") + def test_03_destroy_detached_volume(self): + """Create volume, attach/detach it, then destroy it when expunge=false and expunge=true + + Steps: + # 1. create vm without data disk, resource count increases. + # 2. create volume, resource count increases. + # 3. attach volume to a vm. resource count no changes. + # 4. detach volume from a vm. resource count no changes. + # 5. destroy volume (expunge = false), volume is Destroy. resource count decreased with size of volume. + # 6. destroy volume (expunge = true), volume is not found. resource count no changes. + # 7. destroy vm (expunge=True). resource count decreased with size of root disk + """ + # Create vm + try: + virtual_machine_2 = VirtualMachine.create( + self.apiclient, + self.services["virtual_machine"], + accountid=self.account.name, + domainid=self.account.domainid, + serviceofferingid=self.service_offering.id, + templateid=self.template.id, + zoneid=self.zone.id + ) + except Exception as e: + self.fail("Exception while deploying virtual machine: %s" % e) + + self.expectedCount = self.expectedCount + self.templatesize + self.volumeTotal = self.volumeTotal + 1 + self.verify_resource_count_primary_storage(self.expectedCount, self.volumeTotal); + + # Create volume + volume = Volume.create( + self.apiclient, self.services["volume"], + zoneid=self.zone.id, account=self.account.name, + domainid=self.account.domainid, diskofferingid=self.disk_offering.id + ) + self.expectedCount = self.expectedCount + self.disk_offering.disksize + self.volumeTotal = self.volumeTotal + 1 + self.verify_resource_count_primary_storage(self.expectedCount, self.volumeTotal); + + # Attach volume to vm + virtual_machine_2.attach_volume(self.apiclient, volume) + self.verify_resource_count_primary_storage(self.expectedCount, self.volumeTotal); + + # Detach volume from vm + virtual_machine_2.detach_volume(self.apiclient, volume) + self.verify_resource_count_primary_storage(self.expectedCount, self.volumeTotal); + + # Destroy volume (expunge=False) + volume.destroy(self.apiclient) + self.expectedCount = self.expectedCount - self.disk_offering.disksize + self.volumeTotal = self.volumeTotal - 1 + self.verify_resource_count_primary_storage(self.expectedCount, self.volumeTotal); + + # Destroy volume (expunge=True) + volume.destroy(self.apiclient, expunge=True) + self.verify_resource_count_primary_storage(self.expectedCount, self.volumeTotal); + + # Destroy VM (expunge=True) + virtual_machine_2.delete(self.apiclient, expunge=True) + self.expectedCount = self.expectedCount - self.templatesize + self.volumeTotal = self.volumeTotal - 1 + self.verify_resource_count_primary_storage(self.expectedCount, self.volumeTotal); + + @attr(tags=["advanced", "advancedsg"], required_hardware="false") + def test_04_recover_root_volume_after_restorevm(self): + """Restore VM, recover/delete old root disk + + Steps: + # 1. create vm without data disk, resource count increases. + # 2. restore vm. resource count no changes. + # 3. check old root disk , should be Destroy state + # 4. recover old root disk. resource count increases. + # 5. delete old root disk . resource count decreases. + # 6. destroy vm (expunge=True). resource count decreased with size of root disk + """ + + # Create vm + try: + virtual_machine_3 = VirtualMachine.create( + self.apiclient, + self.services["virtual_machine"], + accountid=self.account.name, + domainid=self.account.domainid, + serviceofferingid=self.service_offering.id, + templateid=self.template.id, + zoneid=self.zone.id + ) + except Exception as e: + self.fail("Exception while deploying virtual machine: %s" % e) + + self.expectedCount = self.expectedCount + self.templatesize + self.volumeTotal = self.volumeTotal + 1 + self.verify_resource_count_primary_storage(self.expectedCount, self.volumeTotal); + + # Get id of root disk + root_volumes_list = Volume.list( + self.apiclient, + virtualmachineid=virtual_machine_3.id, + type='ROOT', + listall=True + ) + status = validateList(root_volumes_list) + self.assertEqual(status[0], PASS, "ROOT Volume List Validation Failed") + root_volume_id = root_volumes_list[0].id + + # restore vm + virtual_machine_3.restore(self.apiclient) + self.verify_resource_count_primary_storage(self.expectedCount, self.volumeTotal); + + # check old root disk state + root_volumes_list = Volume.list( + self.apiclient, + id=root_volume_id, + listall=True + ) + status = validateList(root_volumes_list) + self.assertEqual(status[0], PASS, "ROOT Volume List Validation Failed") + root_volume = root_volumes_list[0] + self.assertEqual(root_volume['state'], 'Destroy', "ROOT volume should be Destroy after restorevm") + + # recover old root disk + cmd = recoverVolume.recoverVolumeCmd() + cmd.id = root_volume.id + self.apiclient.recoverVolume(cmd) + self.expectedCount = self.expectedCount + self.templatesize + self.volumeTotal = self.volumeTotal + 1 + self.verify_resource_count_primary_storage(self.expectedCount, self.volumeTotal); + + # delete old root disk + cmd = deleteVolume.deleteVolumeCmd() + cmd.id = root_volume.id + self.apiclient.deleteVolume(cmd) + self.expectedCount = self.expectedCount - self.templatesize + self.volumeTotal = self.volumeTotal - 1 + self.verify_resource_count_primary_storage(self.expectedCount, self.volumeTotal) + + # Destroy VM (expunge=True) + virtual_machine_3.delete(self.apiclient, expunge=True) + self.expectedCount = self.expectedCount - self.templatesize + self.volumeTotal = self.volumeTotal - 1 + self.verify_resource_count_primary_storage(self.expectedCount, self.volumeTotal); + + @attr(tags=["advanced", "advancedsg"], required_hardware="false") + def test_05_extract_root_volume_and_destroy_vm(self): + """Create VM, extract root volume, then destroy vm and volume + + Steps: + # 1. create vm without data disk, resource count increases. + # 2. stop vm + # 3. extract root volume + # 4. expunge vm, root volume in Expunged state. resource count decreased with size of root disk. + # 5. destroy volume (expunge = false), Exception happened. resource count no changes + # 6. destroy volume (expunge = true). volume is not found. resource count no changes. + """ + + # Create vm + try: + virtual_machine_4 = VirtualMachine.create( + self.apiclient, + self.services["virtual_machine"], + accountid=self.account.name, + domainid=self.account.domainid, + serviceofferingid=self.service_offering.id, + templateid=self.template.id, + zoneid=self.zone.id + ) + except Exception as e: + self.fail("Exception while deploying virtual machine: %s" % e) + + self.expectedCount = self.expectedCount + self.templatesize + self.volumeTotal = self.volumeTotal + 1 + self.verify_resource_count_primary_storage(self.expectedCount, self.volumeTotal); + + # Get id of root disk + root_volumes_list = Volume.list( + self.apiclient, + virtualmachineid=virtual_machine_4.id, + type='ROOT', + listall=True + ) + status = validateList(root_volumes_list) + self.assertEqual(status[0], PASS, "ROOT Volume List Validation Failed") + root_volume_id = root_volumes_list[0].id + + # Stop vm + virtual_machine_4.stop(self.apiclient) + + # extract root volume + cmd = extractVolume.extractVolumeCmd() + cmd.id = root_volume_id + cmd.mode = "HTTP_DOWNLOAD" + cmd.zoneid = self.zone.id + self.apiclient.extractVolume(cmd) + + # Destroy VM (expunge=True) + virtual_machine_4.delete(self.apiclient, expunge=True) + self.expectedCount = self.expectedCount - self.templatesize + self.volumeTotal = self.volumeTotal - 1 + self.verify_resource_count_primary_storage(self.expectedCount, self.volumeTotal); + + # check root disk state + root_volumes_list = Volume.list( + self.apiclient, + id=root_volume_id, + listall=True + ) + status = validateList(root_volumes_list) + self.assertEqual(status[0], PASS, "ROOT Volume List Validation Failed") + root_volume = root_volumes_list[0] + self.assertEqual(root_volume['state'], 'Expunged', "ROOT volume should be Destroy after restorevm") + + # delete root disk + cmd = deleteVolume.deleteVolumeCmd() + cmd.id = root_volume.id + self.apiclient.deleteVolume(cmd) + self.verify_resource_count_primary_storage(self.expectedCount, self.volumeTotal) + + @attr(tags=["advanced", "advancedsg"], required_hardware="false") + def test_06_delete_network(self): + """Delete account network, resource count should not be changed + + Steps: + # 1. Delete account network + # 2. resource count should not be changed + """ + self.account_network.delete(self.apiclient) + self.verify_resource_count_primary_storage(self.expectedCount, self.volumeTotal) diff --git a/tools/marvin/marvin/lib/base.py b/tools/marvin/marvin/lib/base.py index 3bd1064271f..d233c97be48 100755 --- a/tools/marvin/marvin/lib/base.py +++ b/tools/marvin/marvin/lib/base.py @@ -1080,6 +1080,19 @@ class Volume: cmd.id = self.id apiclient.deleteVolume(cmd) + def destroy(self, apiclient, expunge=False): + """Destroy Volume""" + cmd = destroyVolume.destroyVolumeCmd() + cmd.id = self.id + cmd.expunge = expunge + apiclient.destroyVolume(cmd) + + def recover(self, apiclient): + """Recover Volume""" + cmd = recoverVolume.recoverVolumeCmd() + cmd.id = self.id + apiclient.recoverVolume(cmd) + @classmethod def list(cls, apiclient, **kwargs): """List all volumes matching criteria""" diff --git a/ui/l10n/ar.js b/ui/l10n/ar.js index 55622b2e986..acfc5539c2a 100644 --- a/ui/l10n/ar.js +++ b/ui/l10n/ar.js @@ -180,6 +180,7 @@ var dictionary = { "label.action.destroy.instance.processing": "Destroying Instance....", "label.action.destroy.systemvm": "Destroy System VM", "label.action.destroy.systemvm.processing": "Destroying System VM....", + "label.action.destroy.volume":"Destroy Volume", "label.action.detach.disk": "Detach Disk", "label.action.detach.disk.processing": "Detaching Disk....", "label.action.detach.iso": "Detach ISO", @@ -258,6 +259,7 @@ var dictionary = { "label.action.reboot.router.processing": "Rebooting Router....", "label.action.reboot.systemvm": "Reboot System VM", "label.action.reboot.systemvm.processing": "Rebooting System VM....", + "label.action.recover.volume":"Recover Volume", "label.action.recurring.snapshot": "Recurring Snapshots", "label.action.register.iso": "Register ISO", "label.action.register.template": "Register Template from URL", @@ -1847,6 +1849,7 @@ var dictionary = { "message.action.delete.zone": "Please confirm that you want to delete this zone.", "message.action.destroy.instance": "Please confirm that you want to destroy this instance.", "message.action.destroy.systemvm": "Please confirm that you want to destroy this System VM.", + "message.action.destroy.volume":"Please confirm that you want to destroy this volume.", "message.action.disable.cluster": "Please confirm that you want to disable this cluster.", "message.action.disable.nexusVswitch": "Please confirm that you want to disable this nexus 1000v", "message.action.disable.physical.network": "فضلا ، أكّد أنك تريد تعطيل هذه الشبكة الفيزيائية", @@ -1871,6 +1874,7 @@ var dictionary = { "message.action.reboot.instance": "Please confirm that you want to reboot this instance.", "message.action.reboot.router": "All services provided by this virtual router will be interrupted. Please confirm that you want to reboot this router.", "message.action.reboot.systemvm": "Please confirm that you want to reboot this system VM.", + "message.action.recover.volume":"Please confirm that you would like to recover this volume.", "message.action.release.ip": "Please confirm that you want to release this IP.", "message.action.remove.host": "Please confirm that you want to remove this host.", "message.action.reset.password.off": "Your instance currently does not support this feature.", diff --git a/ui/l10n/ca.js b/ui/l10n/ca.js index 0c55524141d..3a7c0459402 100644 --- a/ui/l10n/ca.js +++ b/ui/l10n/ca.js @@ -180,6 +180,7 @@ var dictionary = { "label.action.destroy.instance.processing": "Destroying Instance....", "label.action.destroy.systemvm": "Destroy System VM", "label.action.destroy.systemvm.processing": "Destroying System VM....", + "label.action.destroy.volume":"Destroy Volume", "label.action.detach.disk": "Detach Disk", "label.action.detach.disk.processing": "Detaching Disk....", "label.action.detach.iso": "Detach ISO", @@ -258,6 +259,7 @@ var dictionary = { "label.action.reboot.router.processing": "Rebooting Router....", "label.action.reboot.systemvm": "Reboot System VM", "label.action.reboot.systemvm.processing": "Rebooting System VM....", + "label.action.recover.volume":"Recover Volume", "label.action.recurring.snapshot": "Recurring Snapshots", "label.action.register.iso": "Register ISO", "label.action.register.template": "Register Template from URL", @@ -1847,6 +1849,7 @@ var dictionary = { "message.action.delete.zone": "Please confirm that you want to delete this zone.", "message.action.destroy.instance": "Please confirm that you want to destroy this instance.", "message.action.destroy.systemvm": "Please confirm that you want to destroy this System VM.", + "message.action.destroy.volume":"Please confirm that you want to destroy this volume.", "message.action.disable.cluster": "Please confirm that you want to disable this cluster.", "message.action.disable.nexusVswitch": "Please confirm that you want to disable this nexus 1000v", "message.action.disable.physical.network": "Please confirm that you want to disable this physical network.", @@ -1871,6 +1874,7 @@ var dictionary = { "message.action.reboot.instance": "Please confirm that you want to reboot this instance.", "message.action.reboot.router": "All services provided by this virtual router will be interrupted. Please confirm that you want to reboot this router.", "message.action.reboot.systemvm": "Please confirm that you want to reboot this system VM.", + "message.action.recover.volume":"Please confirm that you would like to recover this volume.", "message.action.release.ip": "Please confirm that you want to release this IP.", "message.action.remove.host": "Please confirm that you want to remove this host.", "message.action.reset.password.off": "Your instance currently does not support this feature.", diff --git a/ui/l10n/de_DE.js b/ui/l10n/de_DE.js index ca1344b87b7..4fbc827c013 100644 --- a/ui/l10n/de_DE.js +++ b/ui/l10n/de_DE.js @@ -180,6 +180,7 @@ var dictionary = { "label.action.destroy.instance.processing": "Instanz wird zerstört....", "label.action.destroy.systemvm": "System-VM vernichten", "label.action.destroy.systemvm.processing": "System-VM wird zerstört....", + "label.action.destroy.volume":"Destroy Volume", "label.action.detach.disk": "Festplatte loslösen", "label.action.detach.disk.processing": "Festplatte wird losgelöst...", "label.action.detach.iso": "ISO loslösen", @@ -258,6 +259,7 @@ var dictionary = { "label.action.reboot.router.processing": "Router wird neu gebootet....", "label.action.reboot.systemvm": "System-VM neu starten", "label.action.reboot.systemvm.processing": "System-VM wird neu gebootet....", + "label.action.recover.volume":"Recover Volume", "label.action.recurring.snapshot": "Wiederkehrende Schnappschüsse", "label.action.register.iso": "ISO registrieren", "label.action.register.template": "Vorlage von URL registrieren", @@ -1849,6 +1851,7 @@ var dictionary = { "message.action.delete.zone": "Bitte bestätigen Sie, dass Sie diese Zone löschen möchten.", "message.action.destroy.instance": "Bitte bestätigen Sie, dass Sie diese Instanz löschen möchten.", "message.action.destroy.systemvm": "Bitte bestätigen Sie, dass Sie diese System-VM zerstören möchten.", + "message.action.destroy.volume":"Please confirm that you want to destroy this volume.", "message.action.disable.cluster": "Bitte bestätigen Sie, dass Sie diesen Cluster deaktivieren möchten.", "message.action.disable.nexusVswitch": "Bitte bestätigen Sie, dass sie diesen nexus 1000v deaktivieren möchten.", "message.action.disable.physical.network": "Bitte bestätigen Sie, dass Sie dieses physikalische Netzwerk deaktivieren möchten.", @@ -1873,6 +1876,7 @@ var dictionary = { "message.action.reboot.instance": "Bitte bestätigen Sie, dass Sie diese Instanz neu starten möchten.", "message.action.reboot.router": "Alle angebotenen Dienste dieses Routers werden unterbrochen. Bitte bestätigen Sie, dass Sie den Router neu starten möchten.", "message.action.reboot.systemvm": "Bitte bestätigen Sie, dass Sie diese System-VM neu starten möchten.", + "message.action.recover.volume":"Please confirm that you would like to recover this volume.", "message.action.release.ip": "Bitte bestätigen Sie, dass Sie diese IP freigeben möchten.", "message.action.remove.host": "Bitte bestätigen Sie, dass Sie diesen Host entfernen möchten.", "message.action.reset.password.off": "Ihre Instanz unterschützt derzeitig nicht dieses Feature.", diff --git a/ui/l10n/en.js b/ui/l10n/en.js index c9fd01b5873..5fdcb08e7f7 100644 --- a/ui/l10n/en.js +++ b/ui/l10n/en.js @@ -182,6 +182,7 @@ var dictionary = { "label.action.destroy.instance.processing":"Destroying Instance....", "label.action.destroy.systemvm":"Destroy System VM", "label.action.destroy.systemvm.processing":"Destroying System VM....", +"label.action.destroy.volume":"Destroy Volume", "label.action.detach.disk":"Detach Disk", "label.action.detach.disk.processing":"Detaching Disk....", "label.action.detach.iso":"Detach ISO", @@ -261,6 +262,7 @@ var dictionary = { "label.action.reboot.router.processing":"Rebooting Router....", "label.action.reboot.systemvm":"Reboot System VM", "label.action.reboot.systemvm.processing":"Rebooting System VM....", +"label.action.recover.volume":"Recover Volume", "label.action.recurring.snapshot":"Recurring Snapshots", "label.action.register.iso":"Register ISO", "label.action.register.ncc":"Register NCC", @@ -1945,6 +1947,7 @@ var dictionary = { "message.action.delete.zone":"Please confirm that you want to delete this zone.", "message.action.destroy.instance":"Please confirm that you want to destroy this instance.", "message.action.destroy.systemvm":"Please confirm that you want to destroy this System VM.", +"message.action.destroy.volume":"Please confirm that you want to destroy this volume.", "message.action.disable.cluster":"Please confirm that you want to disable this cluster.", "message.action.disable.nexusVswitch":"Please confirm that you want to disable this nexus 1000v", "message.action.disable.physical.network":"Please confirm that you want to disable this physical network.", @@ -1969,6 +1972,7 @@ var dictionary = { "message.action.reboot.instance":"Please confirm that you want to reboot this instance.", "message.action.reboot.router":"All services provided by this virtual router will be interrupted. Please confirm that you want to reboot this router.", "message.action.reboot.systemvm":"Please confirm that you want to reboot this system VM.", +"message.action.recover.volume":"Please confirm that you would like to recover this volume.", "message.action.release.ip":"Please confirm that you want to release this IP.", "message.action.remove.host":"Please confirm that you want to remove this host.", "message.action.reset.password.off":"Your instance currently does not support this feature.", diff --git a/ui/l10n/es.js b/ui/l10n/es.js index a7af4e9c539..875b7a16d32 100644 --- a/ui/l10n/es.js +++ b/ui/l10n/es.js @@ -180,6 +180,7 @@ var dictionary = { "label.action.destroy.instance.processing": "Destruyendo Instancia ....", "label.action.destroy.systemvm": "Destruye MV de Sistema", "label.action.destroy.systemvm.processing": "Destruyendo MV de Sistema...", + "label.action.destroy.volume":"Destroy Volume", "label.action.detach.disk": "Desconectar Disco", "label.action.detach.disk.processing": "Desconectando Disco ....", "label.action.detach.iso": "Desconectar ISO", @@ -258,6 +259,7 @@ var dictionary = { "label.action.reboot.router.processing": "Reiniciando Router ....", "label.action.reboot.systemvm": "Reiniciar MV de Sistema", "label.action.reboot.systemvm.processing": "Reinicando MV de Sistema...", + "label.action.recover.volume":"Recover Volume", "label.action.recurring.snapshot": "Instantáneas Recurrentes", "label.action.register.iso": "Registrar ISO", "label.action.register.template": "Registrar Plantilla desde una URL", @@ -1848,6 +1850,7 @@ var dictionary = { "message.action.delete.zone": "Por favor, confirme que desea eliminar esta Zona. ", "message.action.destroy.instance": "Por favor, confirme que desea destruir esta Instancia.", "message.action.destroy.systemvm": "Por favor, confirme que desea destruir esta MV de Sistema.", + "message.action.destroy.volume":"Please confirm that you want to destroy this volume.", "message.action.disable.cluster": "Por favor, confirme que desea deshabilitar este clúster.", "message.action.disable.nexusVswitch": "Por favor confirme que usted quiere deshabilitar este nexus 1000v", "message.action.disable.physical.network": "Por favor confirmar que usted quiere deshabilitar esta red física", @@ -1872,6 +1875,7 @@ var dictionary = { "message.action.reboot.instance": "Por favor, confirme que desea reiniciar esta Instancia.", "message.action.reboot.router": "Todos los servicios provistos por este router virtual serán interrumpidos. Por favor confirmar que desea reiniciarlo.", "message.action.reboot.systemvm": "Por favor, confirme que desea reiniciar esta MV de Sistema.", + "message.action.recover.volume":"Please confirm that you would like to recover this volume.", "message.action.release.ip": "Por favor, confirme que desea liberar esta IP ", "message.action.remove.host": "Por favor confirme que desea borrar este anfitrión.", "message.action.reset.password.off": "Su instancia no soporta esta característica actualmente.", diff --git a/ui/l10n/fr_FR.js b/ui/l10n/fr_FR.js index 74912f0715e..92eb8de3e8b 100644 --- a/ui/l10n/fr_FR.js +++ b/ui/l10n/fr_FR.js @@ -180,6 +180,7 @@ var dictionary = { "label.action.destroy.instance.processing": "Suppression de l'instance...", "label.action.destroy.systemvm": "Supprimer VM Système", "label.action.destroy.systemvm.processing": "Suppression de la VM Système...", + "label.action.destroy.volume":"Destroy Volume", "label.action.detach.disk": "Détacher le disque", "label.action.detach.disk.processing": "Détachement du disque...", "label.action.detach.iso": "Détacher l'image ISO", @@ -258,6 +259,7 @@ var dictionary = { "label.action.reboot.router.processing": "Redémarrage du routeur...", "label.action.reboot.systemvm": "Redémarrer VM Système", "label.action.reboot.systemvm.processing": "Redémarrage de la VM Système...", + "label.action.recover.volume":"Recover Volume", "label.action.recurring.snapshot": "Instantanés récurrents", "label.action.register.iso": "Enregistrer ISO", "label.action.register.template": "Enregistrer modèle depuis une URL", @@ -1849,6 +1851,7 @@ var dictionary = { "message.action.delete.zone": "Supprimer cette zone ?", "message.action.destroy.instance": "Supprimer cette instance ?", "message.action.destroy.systemvm": "Supprimer cette VM Système ?", + "message.action.destroy.volume":"Please confirm that you want to destroy this volume.", "message.action.disable.cluster": "Désactiver ce cluster ?", "message.action.disable.nexusVswitch": "Confirmer la désactivation de ce Nexus 1000v", "message.action.disable.physical.network": "Confirmer l'activation de ce réseau physique.", @@ -1873,6 +1876,7 @@ var dictionary = { "message.action.reboot.instance": "Redémarrer cette instance ?", "message.action.reboot.router": "Tous les services fournit par ce routeur virtuel vont être interrompus. Confirmer le ré-amorçage de ce routeur.", "message.action.reboot.systemvm": "Redémarrer cette VM Système ?", + "message.action.recover.volume":"Please confirm that you would like to recover this volume.", "message.action.release.ip": "Libérer cette adresse IP ?", "message.action.remove.host": "Êtes-vous sûr que vous voulez supprimer cet hôte.", "message.action.reset.password.off": "Votre instance ne supporte pas pour le moment cette fonctionnalité.", diff --git a/ui/l10n/hu.js b/ui/l10n/hu.js index b077d7bdb7a..6912c1ab624 100644 --- a/ui/l10n/hu.js +++ b/ui/l10n/hu.js @@ -180,6 +180,7 @@ var dictionary = { "label.action.destroy.instance.processing": "Példány elpusztítása...", "label.action.destroy.systemvm": "Rendszer VM elpusztítása", "label.action.destroy.systemvm.processing": "Rendszer VM elpusztítása...", + "label.action.destroy.volume":"Destroy Volume", "label.action.detach.disk": "Merevlemez leválasztása", "label.action.detach.disk.processing": "Merevlemez leválasztása...", "label.action.detach.iso": "ISO leválasztása", @@ -258,6 +259,7 @@ var dictionary = { "label.action.reboot.router.processing": "Router újraindítása...", "label.action.reboot.systemvm": "Rendszer VM újraindítása", "label.action.reboot.systemvm.processing": "Rendszer VM újraindítása", + "label.action.recover.volume":"Recover Volume", "label.action.recurring.snapshot": "Ismétlődő pillanatfelvételek", "label.action.register.iso": "ISO regisztrációja", "label.action.register.template": "Sablon regisztrációja URL-ről", @@ -1847,6 +1849,7 @@ var dictionary = { "message.action.delete.zone": "Erősítsd meg, hogy törölni akarod ezt a zónát!", "message.action.destroy.instance": "Erősítsd meg, hogy el akarod pusztítani ezt a példányt!", "message.action.destroy.systemvm": "Erősítsd meg, hogy el akarod pusztítani ezt a rendszer VM-et!", + "message.action.destroy.volume":"Please confirm that you want to destroy this volume.", "message.action.disable.cluster": "Erősítsd meg, hogy ki akarod kapcsolni ezt a fürtöt!", "message.action.disable.nexusVswitch": "Erősítsd meg, hogy ki akarod kapcsolni ezt a nexus 1000v-t!", "message.action.disable.physical.network": "Erősítsd meg, hogy ki akarod kapcsolni ezt a fizikai hálózatot!", @@ -1871,6 +1874,7 @@ var dictionary = { "message.action.reboot.instance": "Erősítsd meg, hogy újra akarod indítani ezt a példányt!", "message.action.reboot.router": "Minden a router által nyújtott szolgáltatás megszakad. Erősítsd meg, hogy újra akarod indítani a routert!", "message.action.reboot.systemvm": "Erősítsd meg, hogy újra akarod indítani ezt a rendszer VM-et!", + "message.action.recover.volume":"Please confirm that you would like to recover this volume.", "message.action.release.ip": "Erősítsd meg, hogy el akarod engedni ezt az IP címet!", "message.action.remove.host": "Erősítsd meg, hogy törölni akarod ezt a kiszolgálót!", "message.action.reset.password.off": "A példány nem támogatja ezt a lehetőséget.", diff --git a/ui/l10n/it_IT.js b/ui/l10n/it_IT.js index cd7b767bef7..4c3ed120887 100644 --- a/ui/l10n/it_IT.js +++ b/ui/l10n/it_IT.js @@ -180,6 +180,7 @@ var dictionary = { "label.action.destroy.instance.processing": "Rimozione Instanza in corso....", "label.action.destroy.systemvm": "Rimozione VM di sistema", "label.action.destroy.systemvm.processing": "Rimozione VM di Sistema in corso....", + "label.action.destroy.volume":"Destroy Volume", "label.action.detach.disk": "Scollegamento di un Disco", "label.action.detach.disk.processing": "Scollegamento Disco in corso....", "label.action.detach.iso": "Scollegamento immagine ISO", @@ -258,6 +259,7 @@ var dictionary = { "label.action.reboot.router.processing": "Riavvio Router in corso....", "label.action.reboot.systemvm": "Riavvio VM di Sistema", "label.action.reboot.systemvm.processing": "Riavvio VM di Sistema in corso....", + "label.action.recover.volume":"Recover Volume", "label.action.recurring.snapshot": "Snapshot Ricorrenti", "label.action.register.iso": "Registrare una ISO", "label.action.register.template": "Registra un Template da URL", @@ -1847,6 +1849,7 @@ var dictionary = { "message.action.delete.zone": "Please confirm that you want to delete this zone.", "message.action.destroy.instance": "Please confirm that you want to destroy this instance.", "message.action.destroy.systemvm": "Please confirm that you want to destroy this System VM.", + "message.action.destroy.volume":"Please confirm that you want to destroy this volume.", "message.action.disable.cluster": "Please confirm that you want to disable this cluster.", "message.action.disable.nexusVswitch": "Si prega di confermare di voler disabilitare questo nexus 1000v", "message.action.disable.physical.network": "Si prega di confermare di voler disabilitare questa rete fisica.", @@ -1871,6 +1874,7 @@ var dictionary = { "message.action.reboot.instance": "Please confirm that you want to reboot this instance.", "message.action.reboot.router": "Tutti i servizi forniti da questo router virtuale saranno interrotti. Si prega di confermare di voler riavviare questo router.", "message.action.reboot.systemvm": "Please confirm that you want to reboot this system VM.", + "message.action.recover.volume":"Please confirm that you would like to recover this volume.", "message.action.release.ip": "Please confirm that you want to release this IP.", "message.action.remove.host": "Si prega di confermare di voler rimuovere questo host.", "message.action.reset.password.off": "Your instance currently does not support this feature.", diff --git a/ui/l10n/ja_JP.js b/ui/l10n/ja_JP.js index 225e5c5cc6f..b876d334629 100644 --- a/ui/l10n/ja_JP.js +++ b/ui/l10n/ja_JP.js @@ -180,6 +180,7 @@ var dictionary = { "label.action.destroy.instance.processing": "インスタンスを破棄しています...", "label.action.destroy.systemvm": "システム VM の破棄", "label.action.destroy.systemvm.processing": "システム VM を破棄しています...", + "label.action.destroy.volume":"ボリュームの破棄", "label.action.detach.disk": "ディスクのデタッチ", "label.action.detach.disk.processing": "ディスクをデタッチしています...", "label.action.detach.iso": "ISO のデタッチ", @@ -258,6 +259,7 @@ var dictionary = { "label.action.reboot.router.processing": "ルーターを再起動しています...", "label.action.reboot.systemvm": "システム VM の再起動", "label.action.reboot.systemvm.processing": "システム VM を再起動しています...", + "label.action.recover.volume":"ボリュームの復元", "label.action.recurring.snapshot": "定期スナップショット", "label.action.register.iso": "ISO の登録", "label.action.register.template": "URL からのテンプレートの登録", @@ -1849,6 +1851,7 @@ var dictionary = { "message.action.delete.zone": "このゾーンを削除してもよろしいですか?", "message.action.destroy.instance": "このインスタンスを破棄してもよろしいですか?", "message.action.destroy.systemvm": "このシステム VM を破棄してもよろしいですか?", + "message.action.destroy.volume":"このボリュームを破棄してもよろしいですか?", "message.action.disable.cluster": "このクラスターを無効にしてもよろしいですか?", "message.action.disable.nexusVswitch": "この Nexus 1000V を無効にしてもよろしいですか?", "message.action.disable.physical.network": "この物理ネットワークを無効にしてもよろしいですか?", @@ -1873,6 +1876,7 @@ var dictionary = { "message.action.reboot.instance": "このインスタンスを再起動してもよろしいですか?", "message.action.reboot.router": "この仮想ルーターで提供するすべてのサービスが中断されます。このルーターを再起動してもよろしいですか?", "message.action.reboot.systemvm": "このシステム VM を再起動してもよろしいですか?", + "message.action.recover.volume":"このボリュームを復元してもよろしいですか?", "message.action.release.ip": "この IP アドレスを解放してもよろしいですか?", "message.action.remove.host": "このホストを削除してもよろしいですか?", "message.action.reset.password.off": "インスタンスは現在この機能をサポートしていません。", diff --git a/ui/l10n/ko_KR.js b/ui/l10n/ko_KR.js index dd3de7e62d1..bdcae7d5a0e 100644 --- a/ui/l10n/ko_KR.js +++ b/ui/l10n/ko_KR.js @@ -180,6 +180,7 @@ var dictionary = { "label.action.destroy.instance.processing": "인스턴스를 파기하는 중...", "label.action.destroy.systemvm": "시스템 VM 파기", "label.action.destroy.systemvm.processing": "시스템 VM를 파기하는 중...", + "label.action.destroy.volume":"Destroy Volume", "label.action.detach.disk": "디스크 분리", "label.action.detach.disk.processing": "디스크를 분리 하는 중...", "label.action.detach.iso": "ISO 분리", @@ -258,6 +259,7 @@ var dictionary = { "label.action.reboot.router.processing": "라우터를 재시작하는 중...", "label.action.reboot.systemvm": "시스템 VM 재시작", "label.action.reboot.systemvm.processing": "시스템 VM를 재시작하는 중...", + "label.action.recover.volume":"Recover Volume", "label.action.recurring.snapshot": "정기 스냅샷", "label.action.register.iso": "ISO 등록", "label.action.register.template": "Register Template from URL", @@ -1847,6 +1849,7 @@ var dictionary = { "message.action.delete.zone": "현재 Zone을 삭제하시겠습니까?", "message.action.destroy.instance": "현재 인스턴스를 파기하시겠습니까?", "message.action.destroy.systemvm": "현재 시스템 VM를 파기하시겠습니까?", + "message.action.destroy.volume":"Please confirm that you want to destroy this volume.", "message.action.disable.cluster": "현재 클러스터를 사용 안 함으로 하시겠습니까?", "message.action.disable.nexusVswitch": "현재 Nexus 1000V를 사용 안 함으로 하시겠습니까?", "message.action.disable.physical.network": "현재 물리 네트워크를 사용 안 함으로 하시겠습니까?", @@ -1871,6 +1874,7 @@ var dictionary = { "message.action.reboot.instance": "현재 인스턴스를 재시작하시겠습니까?", "message.action.reboot.router": "현재 가상 라우터로 제공하는 모든 서비스가 중단됩니다. 이 라우터를 재시작하시겠습니까?", "message.action.reboot.systemvm": "현재 시스템 VM을 재시작하시겠습니까?", + "message.action.recover.volume":"Please confirm that you would like to recover this volume.", "message.action.release.ip": "현재 IP 주소를 해제하시겠습니까?", "message.action.remove.host": "현재 호스트를 삭제하시겠습니까?", "message.action.reset.password.off": "인스턴스는 현재 기능을 지원 하지 않습니다.", diff --git a/ui/l10n/nb_NO.js b/ui/l10n/nb_NO.js index 98592cba825..364a3fc14a7 100644 --- a/ui/l10n/nb_NO.js +++ b/ui/l10n/nb_NO.js @@ -180,6 +180,7 @@ var dictionary = { "label.action.destroy.instance.processing": "Ødelegger instans....", "label.action.destroy.systemvm": "Slett system VM", "label.action.destroy.systemvm.processing": "Sletter system VM....", + "label.action.destroy.volume":"Destroy Volume", "label.action.detach.disk": "Frakoble disk", "label.action.detach.disk.processing": "Kobler fra disk....", "label.action.detach.iso": "Frakoble ISO", @@ -258,6 +259,7 @@ var dictionary = { "label.action.reboot.router.processing": "Omstaer Instans....", "label.action.reboot.systemvm": "Omstart System VM", "label.action.reboot.systemvm.processing": "Omstarter System VM", + "label.action.recover.volume":"Recover Volume", "label.action.recurring.snapshot": "Gjentagende øyeblikksbilder", "label.action.register.iso": "Registrer ISO", "label.action.register.template": "Registrer mal fra en URL", @@ -1847,6 +1849,7 @@ var dictionary = { "message.action.delete.zone": "Vennligst bekreft at du ønsker å slette denne sone.", "message.action.destroy.instance": "Vennligst bekreft at du ønsker å fjerne denne instansen.", "message.action.destroy.systemvm": "Vennligst bekreft at du ønsker å ødelegge denne System VM.", + "message.action.destroy.volume":"Please confirm that you want to destroy this volume.", "message.action.disable.cluster": "Vennligst bekreft at du ønsker å detaktivere denne klyngen.", "message.action.disable.nexusVswitch": "Vennligst bekreft at du ønsker å deaktivere denne nexus 1000v", "message.action.disable.physical.network": "Vennligst bekreft at du ønsker å deaktivere dette fysiske nettverket.", @@ -1871,6 +1874,7 @@ var dictionary = { "message.action.reboot.instance": "Vennligst bekreft at du vill restarte denne instansen.", "message.action.reboot.router": "Alle tjenester levert fra denne virtuelle ruter vil bli avbrutt. Vennligst bekreft at du ønsker å restarte denne ruteren.", "message.action.reboot.systemvm": "Vennligst bekreft at du vil restarte denne system VM", + "message.action.recover.volume":"Please confirm that you would like to recover this volume.", "message.action.release.ip": "Vennligst bekreft at du ønsker å frigi denne IP.", "message.action.remove.host": "Vennligst bekreft at du vil gjerne denne tjeneren.", "message.action.reset.password.off": "Din instans støtter foreløpig ikke denne funksjonen.", diff --git a/ui/l10n/nl_NL.js b/ui/l10n/nl_NL.js index 882598b2d4d..ffb1f1e5030 100644 --- a/ui/l10n/nl_NL.js +++ b/ui/l10n/nl_NL.js @@ -180,6 +180,7 @@ var dictionary = { "label.action.destroy.instance.processing": "Bezig met vernietigen van Instantie....", "label.action.destroy.systemvm": "Vernietig Systeem VM", "label.action.destroy.systemvm.processing": "Bezig met vernietigen van Systeem VM....", + "label.action.destroy.volume":"Vernietig schijf", "label.action.detach.disk": "Ontkoppel Schijf", "label.action.detach.disk.processing": "Bezig met ontkoppelen van Schijf....", "label.action.detach.iso": "Ontkoppel ISO", @@ -258,6 +259,7 @@ var dictionary = { "label.action.reboot.router.processing": "Bezig met herstarten van Router....", "label.action.reboot.systemvm": "Herstart Systeem VM", "label.action.reboot.systemvm.processing": "Bezig met herstarten van Systeem VM....", + "label.action.recover.volume":"Herstel schijf", "label.action.recurring.snapshot": "Terugkerende Snapshots", "label.action.register.iso": "Registreer ISO", "label.action.register.template": "Registreer een template van een URL", @@ -1847,6 +1849,7 @@ var dictionary = { "message.action.delete.zone": "Bevestig dat u deze zone wilt verwijderen", "message.action.destroy.instance": "Bevestig dat u deze instantie wilt vernietigen", "message.action.destroy.systemvm": "Bevestig dat u deze Systeem VM wilt vernietigen", + "message.action.destroy.volume":"Bevestig alstublieft dat U deze schijf wilt vernietigen?", "message.action.disable.cluster": "Bevestig dat u dit cluster wilt uitschakelen.", "message.action.disable.nexusVswitch": "Bevestig dat u deze nexus 1000v wilt uitschakelen.", "message.action.disable.physical.network": "Bevestig dat u dit fysieke netwerk wilt uitschakelen.", @@ -1871,6 +1874,7 @@ var dictionary = { "message.action.reboot.instance": "Bevestig dat u deze instantie wilt herstarten.", "message.action.reboot.router": "Als u deze router herstarten zullen de diensten op de router verstoord worden. Weet u zeker dat u deze actie wil uitvoeren?", "message.action.reboot.systemvm": "Bevestig dat u deze Systeem VM wilt herstarten.", + "message.action.recover.volume":"Bevestig alstublieft dat U deze schijf wilt herstellen?", "message.action.release.ip": "Bevestigd dat u dit IP adres wilt los koppelen.", "message.action.remove.host": "Bevestig dat u deze host wilt verwijderen.", "message.action.reset.password.off": "Uw instantie ondersteunt deze functie momenteel niet.", diff --git a/ui/l10n/pl.js b/ui/l10n/pl.js index ec3daccc3a8..7f993c66803 100644 --- a/ui/l10n/pl.js +++ b/ui/l10n/pl.js @@ -180,6 +180,7 @@ var dictionary = { "label.action.destroy.instance.processing": "Usuwam instancję", "label.action.destroy.systemvm": "Destroy System VM", "label.action.destroy.systemvm.processing": "Destroying System VM....", + "label.action.destroy.volume":"Destroy Volume", "label.action.detach.disk": "Odłącz dysk", "label.action.detach.disk.processing": "Odłączanie dysku....", "label.action.detach.iso": "Odłącz obraz ISO", @@ -258,6 +259,7 @@ var dictionary = { "label.action.reboot.router.processing": "Restartuje router.....", "label.action.reboot.systemvm": "Restartuj system VM", "label.action.reboot.systemvm.processing": "Restartuje system VM....", + "label.action.recover.volume":"Recover Volume", "label.action.recurring.snapshot": "Recurring Snapshots", "label.action.register.iso": "Rejestruj ISO", "label.action.register.template": "Register Template from URL", @@ -1847,6 +1849,7 @@ var dictionary = { "message.action.delete.zone": "Please confirm that you want to delete this zone.", "message.action.destroy.instance": "Please confirm that you want to destroy this instance.", "message.action.destroy.systemvm": "Please confirm that you want to destroy this System VM.", + "message.action.destroy.volume":"Please confirm that you want to destroy this volume.", "message.action.disable.cluster": "Please confirm that you want to disable this cluster.", "message.action.disable.nexusVswitch": "Please confirm that you want to disable this nexus 1000v", "message.action.disable.physical.network": "Please confirm that you want to disable this physical network.", @@ -1871,6 +1874,7 @@ var dictionary = { "message.action.reboot.instance": "Please confirm that you want to reboot this instance.", "message.action.reboot.router": "All services provided by this virtual router will be interrupted. Please confirm that you want to reboot this router.", "message.action.reboot.systemvm": "Please confirm that you want to reboot this system VM.", + "message.action.recover.volume":"Please confirm that you would like to recover this volume.", "message.action.release.ip": "Please confirm that you want to release this IP.", "message.action.remove.host": "Please confirm that you want to remove this host.", "message.action.reset.password.off": "Your instance currently does not support this feature.", diff --git a/ui/l10n/pt_BR.js b/ui/l10n/pt_BR.js index 43f7252bf22..d3969a3dd48 100644 --- a/ui/l10n/pt_BR.js +++ b/ui/l10n/pt_BR.js @@ -180,6 +180,7 @@ var dictionary = { "label.action.destroy.instance.processing": "Apagando Instância....", "label.action.destroy.systemvm": "Apagar VM de Sistema", "label.action.destroy.systemvm.processing": "Apagando VM de Sistema....", + "label.action.destroy.volume":"Destroy Volume", "label.action.detach.disk": "Desplugar Disco", "label.action.detach.disk.processing": "Desplugando Disco....", "label.action.detach.iso": "Desplugar ISO", @@ -258,6 +259,7 @@ var dictionary = { "label.action.reboot.router.processing": "Reiniciando Roteador....", "label.action.reboot.systemvm": "Reiniciar VM de Sistema", "label.action.reboot.systemvm.processing": "Reiniciando VM de Sistema....", + "label.action.recover.volume":"Recover Volume", "label.action.recurring.snapshot": "Snapshots recorrentes", "label.action.register.iso": "Registrar ISO", "label.action.register.template": "Registrar Template da URL", @@ -1847,6 +1849,7 @@ var dictionary = { "message.action.delete.zone": "Confirme que você deseja remover esta Zona.", "message.action.destroy.instance": "Por favor, confirme que você deseja excluir esta Instância.", "message.action.destroy.systemvm": "Confirme que você deseja excluir esta VM de Sistema.", + "message.action.destroy.volume":"Please confirm that you want to destroy this volume.", "message.action.disable.cluster": "Confirma a desativação do cluster.", "message.action.disable.nexusVswitch": "Por favor confirme que voc� deseja desabilitar este nexusVswitch", "message.action.disable.physical.network": "Por favor confirme que você deseja desabilitar esta rede física.", @@ -1871,6 +1874,7 @@ var dictionary = { "message.action.reboot.instance": "Por favor, confirme que você deseja reiniciar esta instância.", "message.action.reboot.router": "Confirme que voc� deseja reiniciar este roteador.", "message.action.reboot.systemvm": "Confirme que você deseja reiniciar esta VM de sistema.", + "message.action.recover.volume":"Please confirm that you would like to recover this volume.", "message.action.release.ip": "Confirme que você deseja liberar este IP.", "message.action.remove.host": "Favor confirmar que você deseja remover este host.", "message.action.reset.password.off": "Sua Instância não suporta esta funcionalidade.", diff --git a/ui/l10n/ru_RU.js b/ui/l10n/ru_RU.js index d20117505cb..49f4d8ae47b 100644 --- a/ui/l10n/ru_RU.js +++ b/ui/l10n/ru_RU.js @@ -180,6 +180,7 @@ var dictionary = { "label.action.destroy.instance.processing": "Уничтожение машины...", "label.action.destroy.systemvm": "Уничтожить системную ВМ", "label.action.destroy.systemvm.processing": "Уничтожение системной ВМ....", + "label.action.destroy.volume":"Destroy Volume", "label.action.detach.disk": "Отсоединить диск", "label.action.detach.disk.processing": "Отсоединение диска....", "label.action.detach.iso": "Отсоединить ISO", @@ -258,6 +259,7 @@ var dictionary = { "label.action.reboot.router.processing": "Перезагрузка роутера...", "label.action.reboot.systemvm": "Перезапустить системную ВМ", "label.action.reboot.systemvm.processing": "Перезагрузка системной ВМ", + "label.action.recover.volume":"Recover Volume", "label.action.recurring.snapshot": "Повторяемые снимки", "label.action.register.iso": "Регистрация ISO", "label.action.register.template": "Регистрация шаблона по URL", @@ -1847,6 +1849,7 @@ var dictionary = { "message.action.delete.zone": "Пожалуйста подтвердите, что Вы хотите удалть эту зону.", "message.action.destroy.instance": "Пожалуйста подтвердите, что Вы хотите уничтожить эту машину.", "message.action.destroy.systemvm": "Подтвердите, что вы действительно хотите удалить эту системную ВМ.", + "message.action.destroy.volume":"Please confirm that you want to destroy this volume.", "message.action.disable.cluster": "Пожалуйста подтвердите, что Вы хотите отключить данный кластер.", "message.action.disable.nexusVswitch": "Пожалуйста, подтвердите, что вы хотите включить это nexusVswitch.", "message.action.disable.physical.network": "Подтвердите, что вы действительно хотите выключить эту физическую сеть.", @@ -1871,6 +1874,7 @@ var dictionary = { "message.action.reboot.instance": "Подтвердите, что вы действительно хотите перезагрузить эту машину.", "message.action.reboot.router": "Подтвердите, что вы действительно хотите перезагрузить этот роутер.", "message.action.reboot.systemvm": "Подтвердите, что вы действительно хотите запустить эту системную ВМ.", + "message.action.recover.volume":"Please confirm that you would like to recover this volume.", "message.action.release.ip": "Пожалуйста подтвержите желание освободить этот IP адрес.", "message.action.remove.host": "Удаление последнего/единственного сервера в кластере и повторная его установка приведет уничтожению рабочего окружения/базы данных на сервере и сделае гостевые машины непригодными к использованию.", "message.action.reset.password.off": "На данный момент машина не поддерживает данную функцию", diff --git a/ui/l10n/zh_CN.js b/ui/l10n/zh_CN.js index bb5f1d9eecd..26cb7081869 100644 --- a/ui/l10n/zh_CN.js +++ b/ui/l10n/zh_CN.js @@ -180,6 +180,7 @@ var dictionary = { "label.action.destroy.instance.processing": "正在销毁实例...", "label.action.destroy.systemvm": "销毁系统 VM", "label.action.destroy.systemvm.processing": "正在销毁系统 VM...", + "label.action.destroy.volume":"销毁卷", "label.action.detach.disk": "取消附加磁盘", "label.action.detach.disk.processing": "正在取消附加磁盘...", "label.action.detach.iso": "取消附加 ISO", @@ -258,6 +259,7 @@ var dictionary = { "label.action.reboot.router.processing": "正在重新启动路由器...", "label.action.reboot.systemvm": "重新启动系统 VM", "label.action.reboot.systemvm.processing": "正在重新启动系统 VM...", + "label.action.recover.volume":"恢复卷", "label.action.recurring.snapshot": "重现快照", "label.action.register.iso": "注册 ISO", "label.action.register.template": "使用URL注册模板", @@ -1849,6 +1851,7 @@ var dictionary = { "message.action.delete.zone": "请确认您确实要删除此资源域。", "message.action.destroy.instance": "请确认您确实要销毁此实例。", "message.action.destroy.systemvm": "请确认您确实要销毁此系统 VM。", + "message.action.destroy.volume":"你确定要销毁这个卷吗?", "message.action.disable.cluster": "请确认您确实要禁用此群集。", "message.action.disable.nexusVswitch": "请确认您确实要禁用此 Nexus 1000v", "message.action.disable.physical.network": "请确认您确实要禁用此物理网络。", @@ -1873,6 +1876,7 @@ var dictionary = { "message.action.reboot.instance": "请确认您确实要重新启动此实例。", "message.action.reboot.router": "此虚拟路由器提供的所有服务都将中断。请确认您确实要重新启动此路由器。", "message.action.reboot.systemvm": "请确认您确实要重新启动此系统 VM。", + "message.action.recover.volume":"你确定要恢复这个卷吗?", "message.action.release.ip": "请确认您确实要释放此 IP。", "message.action.remove.host": "请确认您确实要删除此主机。", "message.action.reset.password.off": "您的实例当前不支持此功能。", diff --git a/ui/scripts/cloudStack.js b/ui/scripts/cloudStack.js index 97eec9ce653..e6550b73074 100644 --- a/ui/scripts/cloudStack.js +++ b/ui/scripts/cloudStack.js @@ -206,6 +206,7 @@ } g_allowUserExpungeRecoverVm = json.listcapabilitiesresponse.capability.allowuserexpungerecovervm; + g_allowUserExpungeRecoverVolume = json.listcapabilitiesresponse.capability.allowuserexpungerecovervolume; g_userProjectsEnabled = json.listcapabilitiesresponse.capability.allowusercreateprojects; g_cloudstackversion = json.listcapabilitiesresponse.capability.cloudstackversion; @@ -337,6 +338,7 @@ g_userPublicTemplateEnabled = json.listcapabilitiesresponse.capability.userpublictemplateenabled.toString(); //convert boolean to string if it's boolean } g_allowUserExpungeRecoverVm = json.listcapabilitiesresponse.capability.allowuserexpungerecovervm; + g_allowUserExpungeRecoverVolume = json.listcapabilitiesresponse.capability.allowuserexpungerecovervolume; g_userProjectsEnabled = json.listcapabilitiesresponse.capability.allowusercreateprojects; g_cloudstackversion = json.listcapabilitiesresponse.capability.cloudstackversion; diff --git a/ui/scripts/sharedFunctions.js b/ui/scripts/sharedFunctions.js index f8ad7bfff84..fa2a661ae9e 100644 --- a/ui/scripts/sharedFunctions.js +++ b/ui/scripts/sharedFunctions.js @@ -31,6 +31,7 @@ var g_kvmsnapshotenabled = null; var g_regionsecondaryenabled = null; var g_userPublicTemplateEnabled = "true"; var g_allowUserExpungeRecoverVm = "false"; +var g_allowUserExpungeRecoverVolume = "false"; var g_cloudstackversion = null; var g_queryAsyncJobResultInterval = 3000; var g_idpList = null; diff --git a/ui/scripts/storage.js b/ui/scripts/storage.js index ab9747990eb..6cb45faf1e8 100644 --- a/ui/scripts/storage.js +++ b/ui/scripts/storage.js @@ -213,6 +213,9 @@ zonename: { label: 'label.zone' }, + vmdisplayname: { + label: 'label.vm.display.name' + }, state: { label: 'label.metrics.state', converter: function (str) { @@ -224,6 +227,7 @@ 'Ready': 'on', 'Destroy': 'off', 'Expunging': 'off', + 'Expunged': 'off', 'Migrating': 'warning', 'UploadOp': 'warning', 'Snapshotting': 'warning', @@ -817,6 +821,33 @@ } }, + state: { + label: 'label.state', + select: function(args) { + args.response.success({ + data: [{ + name: '', + description: '' + }, { + name: 'Allocated', + description: 'state.Allocated' + }, { + name: 'Ready', + description: 'state.Ready' + }, { + name: 'Destroy', + description: 'state.Destroy' + }, { + name: 'Expunging', + description: 'state.Expunging' + }, { + name: 'Expunged', + description: 'state.Expunged' + }] + }); + } + }, + tagKey: { label: 'label.tag.key' }, @@ -1446,6 +1477,102 @@ } }, + destroy: { + label: 'label.action.destroy.volume', + createForm: { + title: 'label.action.destroy.volume', + desc: 'message.action.destroy.volume', + isWarning: true, + preFilter: function(args) { + if (!isAdmin() && ! g_allowUserExpungeRecoverVolume) { + args.$form.find('.form-item[rel=expunge]').hide(); + } + }, + fields: { + expunge: { + label: 'label.expunge', + isBoolean: true, + isChecked: false + } + } + }, + messages: { + confirm: function(args) { + return 'message.action.destroy.volume'; + }, + notification: function(args) { + return 'label.action.destroy.volume'; + } + }, + action: function(args) { + var data = { + id: args.context.volumes[0].id + }; + if (args.data.expunge == 'on') { + $.extend(data, { + expunge: true + }); + } + $.ajax({ + url: createURL("destroyVolume"), + data: data, + dataType: "json", + async: true, + success: function(json) { + var jid = json.destroyvolumeresponse.jobid; + args.response.success({ + _custom: { + jobId: jid, + getUpdatedItem: function(json) { + if ('volume' in json.queryasyncjobresultresponse.jobresult) { //destroy without expunge + var volume = json.queryasyncjobresultresponse.jobresult.volume; + if (volume.state == 'Expunged') { + return { 'toRemove': true }; + } else { + return volume; + } + } else //destroy with expunge + return { 'toRemove': true }; + }, + getActionFilter: function() { + return volumeActionfilter; + } + } + }); + } + }); + }, + notification: { + poll: pollAsyncJobResult + } + }, + + recover: { + label: 'label.action.recover.volume', + messages: { + confirm: function(args) { + return 'message.action.recover.volume'; + }, + notification: function(args) { + return 'label.action.recover.volume'; + } + }, + action: function(args) { + $.ajax({ + url: createURL("recoverVolume&id=" + args.context.volumes[0].id), + dataType: "json", + success: function(json) { + args.response.success(); + } + }); + }, + notification: { + poll: function(args) { + args.complete(); + } + } + }, + resize: { label: 'label.action.resize.volume', messages: { @@ -2656,6 +2783,15 @@ var jsonObj = args.context.item; var allowedActions = []; + if ((isAdmin() || g_allowUserExpungeRecoverVolume) && jsonObj.state == 'Destroy') { + return ["recover", "remove"]; + } else if (jsonObj.state == 'Destroy') { + return []; + } + + if (jsonObj.state == 'Expunging' || jsonObj.state == 'Expunged') { + return ["remove"]; + } if (jsonObj.state == 'Destroyed' || jsonObj.state == 'Migrating' || jsonObj.state == 'Uploading') { return []; @@ -2710,7 +2846,12 @@ allowedActions.push("detachDisk"); } } else { // Disk not attached - allowedActions.push("remove"); + if (jsonObj.state == "Allocated" || jsonObj.state == "Uploaded") { + allowedActions.push("remove"); + } else { + allowedActions.push("createTemplate"); + allowedActions.push("destroy"); + } if (jsonObj.state == "Ready" && isAdmin()) { allowedActions.push("migrateToAnotherStorage"); }