This is an automated email from the ASF dual-hosted git repository.

dahn pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/cloudstack.git


The following commit(s) were added to refs/heads/main by this push:
     new 61a722548fc Create API to reassign volume (#6938)
61a722548fc is described below

commit 61a722548fcf5c32bd5c2fee49882eeb8d8f3ba1
Author: João Jandre <[email protected]>
AuthorDate: Fri Jan 27 07:10:56 2023 -0300

    Create API to reassign volume (#6938)
---
 .../java/com/cloud/storage/VolumeApiService.java   |   3 +
 .../api/command/user/volume/AssignVolumeCmd.java   | 119 ++++++++++++++++
 .../storage/command/MoveVolumeCommand.java         |  66 +++++++++
 .../subsystem/api/storage/EndPointSelector.java    |   2 +
 .../subsystem/api/storage/VolumeService.java       |   4 +
 .../resources/META-INF/db/schema-41720to41800.sql  |   5 +
 .../storage/endpoint/DefaultEndPointSelector.java  |  12 +-
 .../storage/volume/VolumeServiceImpl.java          |  59 +++++++-
 .../com/cloud/server/ManagementServerImpl.java     |   2 +
 .../com/cloud/storage/VolumeApiServiceImpl.java    | 128 +++++++++++++++++
 .../cloud/storage/VolumeApiServiceImplTest.java    | 157 +++++++++++++++++++++
 .../resource/NfsSecondaryStorageResource.java      |  35 +++++
 .../cloudstack/utils/bytescale/ByteScaleUtils.java |  10 ++
 .../utils/bytescale/ByteScaleUtilsTest.java        |   7 +
 14 files changed, 602 insertions(+), 7 deletions(-)

diff --git a/api/src/main/java/com/cloud/storage/VolumeApiService.java 
b/api/src/main/java/com/cloud/storage/VolumeApiService.java
index 475df50bbef..b85195dafae 100644
--- a/api/src/main/java/com/cloud/storage/VolumeApiService.java
+++ b/api/src/main/java/com/cloud/storage/VolumeApiService.java
@@ -21,6 +21,7 @@ package com.cloud.storage;
 import java.net.MalformedURLException;
 import java.util.Map;
 
+import org.apache.cloudstack.api.command.user.volume.AssignVolumeCmd;
 import org.apache.cloudstack.api.command.user.volume.AttachVolumeCmd;
 import 
org.apache.cloudstack.api.command.user.volume.ChangeOfferingForVolumeCmd;
 import org.apache.cloudstack.api.command.user.volume.CreateVolumeCmd;
@@ -119,6 +120,8 @@ public interface VolumeApiService {
      */
     String extractVolume(ExtractVolumeCmd cmd);
 
+    Volume assignVolumeToAccount(AssignVolumeCmd cmd) throws 
ResourceAllocationException;
+
     boolean isDisplayResourceEnabled(Long id);
 
     void updateDisplay(Volume volume, Boolean displayVolume);
diff --git 
a/api/src/main/java/org/apache/cloudstack/api/command/user/volume/AssignVolumeCmd.java
 
b/api/src/main/java/org/apache/cloudstack/api/command/user/volume/AssignVolumeCmd.java
new file mode 100644
index 00000000000..03413682c4f
--- /dev/null
+++ 
b/api/src/main/java/org/apache/cloudstack/api/command/user/volume/AssignVolumeCmd.java
@@ -0,0 +1,119 @@
+// 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 com.cloud.exception.ResourceAllocationException;
+import com.cloud.user.Account;
+import com.cloud.utils.exception.CloudRuntimeException;
+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.ServerApiException;
+import org.apache.cloudstack.api.command.user.UserCmd;
+import org.apache.cloudstack.api.response.AccountResponse;
+import org.apache.cloudstack.api.response.ProjectResponse;
+import org.apache.cloudstack.api.response.VolumeResponse;
+import org.apache.log4j.Logger;
+
+import com.cloud.storage.Volume;
+
+import java.util.Map;
+
+@APICommand(name = AssignVolumeCmd.CMD_NAME, responseObject = 
VolumeResponse.class, description = "Changes ownership of a Volume from one 
account to another.", entityType = {
+        Volume.class}, requestHasSensitiveInfo = false, 
responseHasSensitiveInfo = false, since = "4.18.0.0")
+public class AssignVolumeCmd extends BaseCmd implements UserCmd {
+    public static final Logger LOGGER = 
Logger.getLogger(AssignVolumeCmd.class.getName());
+    public static final String CMD_NAME = "assignVolume";
+
+    /////////////////////////////////////////////////////
+    //////////////// API parameters /////////////////////
+    /////////////////////////////////////////////////////
+
+    @Parameter(name = ApiConstants.VOLUME_ID, type = CommandType.UUID, 
entityType = VolumeResponse.class, required = true, description = "The ID of 
the volume to be reassigned.")
+    private Long volumeId;
+
+    @Parameter(name = ApiConstants.ACCOUNT_ID, type = CommandType.UUID, 
entityType = AccountResponse.class,
+            description = "The ID of the account to which the volume will be 
assigned. Mutually exclusive with parameter 'projectid'.")
+    private Long accountId;
+
+    @Parameter(name = ApiConstants.PROJECT_ID, type = CommandType.UUID, 
entityType = ProjectResponse.class,
+            description = "The ID of the project to which the volume will be 
assigned. Mutually exclusive with 'accountid'.")
+    private Long projectid;
+
+    /////////////////////////////////////////////////////
+    /////////////////// Accessors ///////////////////////
+    /////////////////////////////////////////////////////
+
+    public Long getVolumeId() {
+        return volumeId;
+    }
+
+    public Long getAccountId() {
+        return accountId;
+    }
+
+    public Long getProjectid() {
+        return projectid;
+    }
+
+    /////////////////////////////////////////////////////
+    /////////////// API Implementation///////////////////
+    /////////////////////////////////////////////////////
+
+    @Override
+    public void execute() {
+        try {
+            Volume result = _volumeService.assignVolumeToAccount(this);
+            if (result == null) {
+                Map<String,String> fullParams = getFullUrlParams();
+                if (accountId != null) {
+                    throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, 
String.format("Failed to move volume [%s] to account [%s].", 
fullParams.get(ApiConstants.VOLUME_ID),
+                            fullParams.get(ApiConstants.ACCOUNT_ID)));
+                }
+                throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, 
String.format("Failed to move volume [%s] to project [%s].", 
fullParams.get(ApiConstants.VOLUME_ID),
+                        fullParams.get(ApiConstants.PROJECT_ID)));
+            }
+
+            VolumeResponse response = 
_responseGenerator.createVolumeResponse(getResponseView(), result);
+            response.setResponseName(getCommandName());
+            setResponseObject(response);
+
+        } catch (CloudRuntimeException | ResourceAllocationException e) {
+            String msg = String.format("Assign volume command for volume [%s] 
failed due to [%s].", getFullUrlParams().get("volumeid"), e.getMessage());
+            LOGGER.error(msg, e);
+            throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, msg);
+        }
+    }
+
+    @Override
+    public String getCommandName() {
+        return CMD_NAME.toLowerCase() + RESPONSE_SUFFIX;
+    }
+
+    @Override
+    public long getEntityOwnerId() {
+        Volume volume = _responseGenerator.findVolumeById(getVolumeId());
+
+        if (volume != null) {
+            return volume.getAccountId();
+        }
+
+        return Account.ACCOUNT_ID_SYSTEM;
+    }
+}
diff --git 
a/core/src/main/java/org/apache/cloudstack/storage/command/MoveVolumeCommand.java
 
b/core/src/main/java/org/apache/cloudstack/storage/command/MoveVolumeCommand.java
new file mode 100644
index 00000000000..568cc426e5e
--- /dev/null
+++ 
b/core/src/main/java/org/apache/cloudstack/storage/command/MoveVolumeCommand.java
@@ -0,0 +1,66 @@
+//
+// 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.storage.command;
+
+import com.cloud.agent.api.Command;
+
+public class MoveVolumeCommand extends Command {
+
+     private String srcPath;
+     private String destPath;
+     private String volumeName;
+     private String volumeUuid;
+     private String datastoreUri;
+
+    public MoveVolumeCommand(String volumeUuid, String volumeName, String 
destPath, String srcPath, String datastoreUri) {
+         this.volumeName = volumeName;
+         this.volumeUuid = volumeUuid;
+         this.srcPath = srcPath;
+         this.destPath = destPath;
+         this.datastoreUri = datastoreUri;
+     }
+
+     public String getSrcPath() {
+         return srcPath;
+     }
+
+     public String getDestPath() {
+         return destPath;
+     }
+
+     public String getVolumeName() {
+         return volumeName;
+     }
+
+     public String getVolumeUuid() {
+         return volumeUuid;
+     }
+
+    public String getDatastoreUri() {
+        return datastoreUri;
+    }
+
+    @Override
+    public boolean executeInSequence() {
+        return false;
+    }
+
+ }
+
diff --git 
a/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/EndPointSelector.java
 
b/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/EndPointSelector.java
index 6f6e79d067e..ec8dfe633b5 100644
--- 
a/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/EndPointSelector.java
+++ 
b/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/EndPointSelector.java
@@ -46,4 +46,6 @@ public interface EndPointSelector {
     EndPoint select(Scope scope, Long storeId);
 
     EndPoint select(DataStore store, String downloadUrl);
+
+    EndPoint findSsvm(long dcId);
 }
diff --git 
a/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/VolumeService.java
 
b/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/VolumeService.java
index 9f3247438c5..ec7c6155092 100644
--- 
a/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/VolumeService.java
+++ 
b/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/VolumeService.java
@@ -30,6 +30,8 @@ import com.cloud.exception.StorageAccessException;
 import com.cloud.host.Host;
 import com.cloud.hypervisor.Hypervisor.HypervisorType;
 import com.cloud.offering.DiskOffering;
+import com.cloud.storage.Volume;
+import com.cloud.user.Account;
 import com.cloud.utils.Pair;
 
 public interface VolumeService {
@@ -108,4 +110,6 @@ public interface VolumeService {
      */
     boolean 
copyPoliciesBetweenVolumesAndDestroySourceVolumeAfterMigration(ObjectInDataStoreStateMachine.Event
 destinationEvent, Answer destinationEventAnswer,
       VolumeInfo sourceVolume, VolumeInfo destinationVolume, boolean 
retryExpungeVolumeAsync);
+
+    void moveVolumeOnSecondaryStorageToAnotherAccount(Volume volume, Account 
sourceAccount, Account destAccount);
 }
\ No newline at end of file
diff --git 
a/engine/schema/src/main/resources/META-INF/db/schema-41720to41800.sql 
b/engine/schema/src/main/resources/META-INF/db/schema-41720to41800.sql
index ae6fcfc57d7..34f138c7540 100644
--- a/engine/schema/src/main/resources/META-INF/db/schema-41720to41800.sql
+++ b/engine/schema/src/main/resources/META-INF/db/schema-41720to41800.sql
@@ -1063,3 +1063,8 @@ CREATE TABLE IF NOT EXISTS `cloud`.`console_session` (
     CONSTRAINT `fk_consolesession__host_id` FOREIGN KEY(`host_id`) REFERENCES 
`cloud`.`host`(`id`),
     CONSTRAINT `uc_consolesession__uuid` UNIQUE (`uuid`)
 );
+
+-- Add assignVolume API permission to default resource admin and domain admin
+INSERT INTO `cloud`.`role_permissions` (`uuid`, `role_id`, `rule`, 
`permission`) VALUES (UUID(), 2, 'assignVolume', 'ALLOW');
+INSERT INTO `cloud`.`role_permissions` (`uuid`, `role_id`, `rule`, 
`permission`) VALUES (UUID(), 3, 'assignVolume', 'ALLOW');
+
diff --git 
a/engine/storage/src/main/java/org/apache/cloudstack/storage/endpoint/DefaultEndPointSelector.java
 
b/engine/storage/src/main/java/org/apache/cloudstack/storage/endpoint/DefaultEndPointSelector.java
index 1b8fb4cc3d7..4c13759c1c1 100644
--- 
a/engine/storage/src/main/java/org/apache/cloudstack/storage/endpoint/DefaultEndPointSelector.java
+++ 
b/engine/storage/src/main/java/org/apache/cloudstack/storage/endpoint/DefaultEndPointSelector.java
@@ -330,9 +330,15 @@ public class DefaultEndPointSelector implements 
EndPointSelector {
         if (storeScope.getScopeType() == ScopeType.ZONE) {
             dcId = storeScope.getScopeId();
         }
-        // find ssvm that can be used to download data to store. For zone-wide
-        // image store, use SSVM for that zone. For region-wide store,
-        // we can arbitrarily pick one ssvm to do that task
+
+        return findSsvm(dcId);
+    }
+
+    /**
+     * Finds an SSVM that can be used to execute a command.
+     * For zone-wide image store, use SSVM for that zone. For region-wide 
store, we can arbitrarily pick one SSVM to do the task.
+     * */
+    public EndPoint findSsvm(long dcId) {
         List<HostVO> ssAHosts = 
listUpAndConnectingSecondaryStorageVmHost(dcId);
         if (ssAHosts == null || ssAHosts.isEmpty()) {
             return null;
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 cd7a840c86f..48de0eb016b 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
@@ -18,6 +18,9 @@
  */
 package org.apache.cloudstack.storage.volume;
 
+
+import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Date;
@@ -25,12 +28,14 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Random;
+import java.util.concurrent.ExecutionException;
 
 import javax.inject.Inject;
 
 import org.apache.cloudstack.secret.dao.PassphraseDao;
 import com.cloud.storage.VMTemplateVO;
 import com.cloud.storage.dao.VMTemplateDao;
+import com.cloud.storage.resource.StorageProcessor;
 import org.apache.cloudstack.annotation.AnnotationService;
 import org.apache.cloudstack.annotation.dao.AnnotationDao;
 import org.apache.cloudstack.engine.cloud.entity.api.VolumeEntity;
@@ -65,6 +70,7 @@ import org.apache.cloudstack.storage.RemoteHostEndPoint;
 import org.apache.cloudstack.storage.command.CommandResult;
 import org.apache.cloudstack.storage.command.CopyCmdAnswer;
 import org.apache.cloudstack.storage.command.DeleteCommand;
+import org.apache.cloudstack.storage.command.MoveVolumeCommand;
 import org.apache.cloudstack.storage.datastore.PrimaryDataStoreProviderManager;
 import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao;
 import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreDao;
@@ -125,7 +131,9 @@ import com.cloud.storage.dao.VolumeDao;
 import com.cloud.storage.dao.VolumeDetailsDao;
 import com.cloud.storage.snapshot.SnapshotApiService;
 import com.cloud.storage.snapshot.SnapshotManager;
+import com.cloud.storage.template.TemplateConstants;
 import com.cloud.storage.template.TemplateProp;
+import com.cloud.user.Account;
 import com.cloud.user.AccountManager;
 import com.cloud.user.ResourceLimitService;
 import com.cloud.utils.NumbersUtil;
@@ -136,9 +144,6 @@ import com.cloud.utils.exception.CloudRuntimeException;
 import com.cloud.vm.VirtualMachine;
 import org.apache.commons.lang3.StringUtils;
 
-import static 
com.cloud.storage.resource.StorageProcessor.REQUEST_TEMPLATE_RELOAD;
-import java.util.concurrent.ExecutionException;
-
 @Component
 public class VolumeServiceImpl implements VolumeService {
     private static final Logger s_logger = 
Logger.getLogger(VolumeServiceImpl.class);
@@ -806,7 +811,7 @@ public class VolumeServiceImpl implements VolumeService {
             // hack for Vmware: host is down, previously download template to 
the host needs to be re-downloaded, so we need to reset
             // template_spool_ref entry here to NOT_DOWNLOADED and Allocated 
state
             Answer ans = result.getAnswer();
-            if (ans != null && ans instanceof CopyCmdAnswer && 
ans.getDetails().contains(REQUEST_TEMPLATE_RELOAD)) {
+            if (ans instanceof CopyCmdAnswer && 
ans.getDetails().contains(StorageProcessor.REQUEST_TEMPLATE_RELOAD)) {
                 if (tmplOnPrimary != null) {
                     s_logger.info("Reset template_spool_ref entry so that 
vmware template can be reloaded in next try");
                     VMTemplateStoragePoolVO templatePoolRef = 
_tmpltPoolDao.findByPoolTemplate(tmplOnPrimary.getDataStore().getId(), 
tmplOnPrimary.getId(), deployAsIsConfiguration);
@@ -2751,4 +2756,50 @@ public class VolumeServiceImpl implements VolumeService {
             volDao.remove(vol.getId());
         }
     }
+
+    @Override
+    public void moveVolumeOnSecondaryStorageToAnotherAccount(Volume volume, 
Account sourceAccount, Account destAccount) {
+        VolumeDataStoreVO volumeStore = 
_volumeStoreDao.findByVolume(volume.getId());
+
+        if (volumeStore == null) {
+            s_logger.debug(String.format("Volume [%s] is not present in the 
secondary storage. Therefore we do not need to move it in the secondary 
storage.", volume));
+            return;
+        }
+        s_logger.debug(String.format("Volume [%s] is present in secondary 
storage. It will be necessary to move it from the source account's [%s] folder 
to the destination "
+                        + "account's [%s] folder.",
+                volume.getUuid(), sourceAccount, destAccount));
+
+        VolumeInfo volumeInfo = volFactory.getVolume(volume.getId(), 
DataStoreRole.Image);
+        String datastoreUri = volumeInfo.getDataStore().getUri();
+        Path srcPath = Paths.get(volumeInfo.getPath());
+        String destPath = buildVolumePath(destAccount.getAccountId(), 
volume.getId());
+
+        EndPoint ssvm = _epSelector.findSsvm(volume.getDataCenterId());
+
+        MoveVolumeCommand cmd = new MoveVolumeCommand(volume.getUuid(), 
volume.getName(), destPath, srcPath.getParent().toString(), datastoreUri);
+
+        Answer answer = ssvm.sendMessage(cmd);
+
+        if (!answer.getResult()) {
+            String msg = String.format("Unable to move volume [%s] from [%s] 
(source account's [%s] folder) to [%s] (destination account's [%s] folder) in 
the secondary storage, due "
+                            + "to [%s].",
+                    volume.getUuid(), srcPath.getParent(), sourceAccount, 
destPath, destAccount, answer.getDetails());
+            s_logger.error(msg);
+            throw new CloudRuntimeException(msg);
+        }
+
+        s_logger.debug(String.format("Volume [%s] was moved from [%s] (source 
account's [%s] folder) to [%s] (destination account's [%s] folder) in the 
secondary storage.",
+                volume.getUuid(), srcPath.getParent(), sourceAccount, 
destPath, destAccount));
+
+        volumeStore.setInstallPath(String.format("%s/%s", destPath, 
srcPath.getFileName().toString()));
+        if (!_volumeStoreDao.update(volumeStore.getId(), volumeStore)) {
+            String msg = String.format("Unable to update volume [%s] install 
path in the DB.", volumeStore.getVolumeId());
+            s_logger.error(msg);
+            throw new CloudRuntimeException(msg);
+        }
+    }
+
+    protected String buildVolumePath(long accountId, long volumeId) {
+        return String.format("%s/%s/%s", 
TemplateConstants.DEFAULT_VOLUME_ROOT_DIR, accountId, volumeId);
+    }
 }
diff --git a/server/src/main/java/com/cloud/server/ManagementServerImpl.java 
b/server/src/main/java/com/cloud/server/ManagementServerImpl.java
index 5fc34659992..9922c275ed7 100644
--- a/server/src/main/java/com/cloud/server/ManagementServerImpl.java
+++ b/server/src/main/java/com/cloud/server/ManagementServerImpl.java
@@ -724,6 +724,7 @@ import 
org.apache.cloudstack.api.command.user.vmsnapshot.DeleteVMSnapshotCmd;
 import org.apache.cloudstack.api.command.user.vmsnapshot.ListVMSnapshotCmd;
 import org.apache.cloudstack.api.command.user.vmsnapshot.RevertToVMSnapshotCmd;
 import org.apache.cloudstack.api.command.user.volume.AddResourceDetailCmd;
+import org.apache.cloudstack.api.command.user.volume.AssignVolumeCmd;
 import org.apache.cloudstack.api.command.user.volume.AttachVolumeCmd;
 import 
org.apache.cloudstack.api.command.user.volume.ChangeOfferingForVolumeCmd;
 import org.apache.cloudstack.api.command.user.volume.CreateVolumeCmd;
@@ -3717,6 +3718,7 @@ public class ManagementServerImpl extends ManagerBase 
implements ManagementServe
         cmdList.add(ListResourceIconCmd.class);
         cmdList.add(PatchSystemVMCmd.class);
         cmdList.add(ListGuestVlansCmd.class);
+        cmdList.add(AssignVolumeCmd.class);
 
         // Out-of-band management APIs for admins
         cmdList.add(EnableOutOfBandManagementForHostCmd.class);
diff --git a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java 
b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java
index e5dbfd1118c..8fae6a99e71 100644
--- a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java
+++ b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java
@@ -33,6 +33,9 @@ import java.util.concurrent.ExecutionException;
 
 import javax.inject.Inject;
 
+import com.cloud.projects.Project;
+import com.cloud.projects.ProjectManager;
+import org.apache.cloudstack.api.command.user.volume.AssignVolumeCmd;
 import org.apache.cloudstack.api.ApiErrorCode;
 import org.apache.cloudstack.api.ServerApiException;
 import org.apache.cloudstack.api.ApiConstants.IoDriverPolicy;
@@ -93,6 +96,7 @@ import 
org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
 import org.apache.cloudstack.storage.datastore.db.VolumeDataStoreDao;
 import org.apache.cloudstack.storage.datastore.db.VolumeDataStoreVO;
 import org.apache.cloudstack.storage.image.datastore.ImageStoreEntity;
+import org.apache.cloudstack.utils.bytescale.ByteScaleUtils;
 import org.apache.cloudstack.utils.identity.ManagementServerNode;
 import org.apache.cloudstack.utils.imagestore.ImageStoreUtil;
 import 
org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils;
@@ -183,6 +187,7 @@ import com.cloud.utils.db.Filter;
 import com.cloud.utils.db.SearchCriteria;
 import com.cloud.utils.db.Transaction;
 import com.cloud.utils.db.TransactionCallback;
+import com.cloud.utils.db.TransactionCallbackNoReturn;
 import com.cloud.utils.db.TransactionCallbackWithException;
 import com.cloud.utils.db.TransactionStatus;
 import com.cloud.utils.db.UUIDManager;
@@ -318,6 +323,9 @@ public class VolumeApiServiceImpl extends ManagerBase 
implements VolumeApiServic
     @Inject
     protected SnapshotHelper snapshotHelper;
 
+    @Inject
+    protected ProjectManager projectManager;
+
     protected Gson _gson;
 
     private static final List<HypervisorType> SupportedHypervisorsForVolResize 
= Arrays.asList(HypervisorType.KVM, HypervisorType.XenServer,
@@ -3745,6 +3753,126 @@ public class VolumeApiServiceImpl extends ManagerBase 
implements VolumeApiServic
         return orchestrateExtractVolume(volume.getId(), zoneId);
     }
 
+    @Override
+    @ActionEvent(eventType = EventTypes.EVENT_VOLUME_CREATE, eventDescription 
= "Assigning volume to new account", async = false)
+    public Volume assignVolumeToAccount(AssignVolumeCmd command) throws 
ResourceAllocationException {
+        Account caller = CallContext.current().getCallingAccount();
+        VolumeVO volume = _volsDao.findById(command.getVolumeId());
+        Map<String, String> fullUrlParams = command.getFullUrlParams();
+
+        validateVolume(fullUrlParams.get("volumeid"), volume);
+
+        Account oldAccount = 
_accountMgr.getActiveAccountById(volume.getAccountId());
+        Account newAccount = 
getAccountOrProject(fullUrlParams.get("projectid"), command.getAccountId(), 
command.getProjectid(), caller);
+
+        validateAccounts(fullUrlParams.get("accountid"), volume, oldAccount, 
newAccount);
+
+        _accountMgr.checkAccess(caller, null, true, oldAccount);
+        _accountMgr.checkAccess(caller, null, true, newAccount);
+
+        _resourceLimitMgr.checkResourceLimit(newAccount, ResourceType.volume, 
ByteScaleUtils.bytesToGibibytes(volume.getSize()));
+        _resourceLimitMgr.checkResourceLimit(newAccount, 
ResourceType.primary_storage, volume.getSize());
+
+        Transaction.execute(new TransactionCallbackNoReturn() {
+            @Override
+            public void doInTransactionWithoutResult(TransactionStatus status) 
{
+                updateVolumeAccount(oldAccount, volume, newAccount);
+            }
+        });
+
+        return volume;
+    }
+
+    protected void updateVolumeAccount(Account oldAccount, VolumeVO volume, 
Account newAccount) {
+        UsageEventUtils.publishUsageEvent(EventTypes.EVENT_VOLUME_DELETE, 
volume.getAccountId(), volume.getDataCenterId(), volume.getId(), 
volume.getName(),
+                Volume.class.getName(), volume.getUuid(), 
volume.isDisplayVolume());
+        _resourceLimitMgr.decrementResourceCount(oldAccount.getAccountId(), 
ResourceType.volume, ByteScaleUtils.bytesToGibibytes(volume.getSize()));
+        _resourceLimitMgr.decrementResourceCount(oldAccount.getAccountId(), 
ResourceType.primary_storage, volume.getSize());
+
+        volume.setAccountId(newAccount.getAccountId());
+        volume.setDomainId(newAccount.getDomainId());
+        _volsDao.persist(volume);
+
+        _resourceLimitMgr.incrementResourceCount(newAccount.getAccountId(), 
ResourceType.volume, ByteScaleUtils.bytesToGibibytes(volume.getSize()));
+        _resourceLimitMgr.incrementResourceCount(newAccount.getAccountId(), 
ResourceType.primary_storage, volume.getSize());
+
+        UsageEventUtils.publishUsageEvent(EventTypes.EVENT_VOLUME_CREATE, 
volume.getAccountId(), volume.getDataCenterId(), volume.getId(), 
volume.getName(),
+                volume.getDiskOfferingId(), volume.getTemplateId(), 
volume.getSize(), Volume.class.getName(),
+                volume.getUuid(), volume.isDisplayVolume());
+
+        volService.moveVolumeOnSecondaryStorageToAnotherAccount(volume, 
oldAccount, newAccount);
+    }
+
+    /**
+     * Validates if the accounts are null, if the new account state is 
correct, and if the two accounts are the same.
+     * Throws {@link InvalidParameterValueException}.
+     * */
+    protected void validateAccounts(String newAccountUuid, VolumeVO volume, 
Account oldAccount, Account newAccount) {
+        if (oldAccount == null) {
+            throw new InvalidParameterValueException(String.format("The 
current account of the volume [%s] is invalid.",
+                    
ReflectionToStringBuilderUtils.reflectOnlySelectedFields(volume, "name", 
"uuid")));
+        }
+
+        if (newAccount == null) {
+            throw new InvalidParameterValueException(String.format("UUID of 
the destination account is invalid. No account was found with UUID [%s].", 
newAccountUuid));
+        }
+
+        if (newAccount.getState() == Account.State.DISABLED || 
newAccount.getState() == Account.State.LOCKED) {
+            throw new InvalidParameterValueException(String.format("Unable to 
assign volume to destination account [%s], as it is in [%s] state.", newAccount,
+                    newAccount.getState().toString()));
+        }
+
+        if (oldAccount.getAccountId() == newAccount.getAccountId()) {
+            throw new InvalidParameterValueException(String.format("The new 
account and the old account are the same [%s].", oldAccount));
+        }
+    }
+
+    /**
+     * Validates if the volume can be reassigned to another account.
+     * Throws {@link InvalidParameterValueException} if volume is null.
+     * Throws {@link PermissionDeniedException} if volume is attached to a VM 
or if it has snapshots.
+     * */
+    protected void validateVolume(String volumeUuid, VolumeVO volume) {
+        if (volume == null) {
+            throw new InvalidParameterValueException(String.format("No volume 
was found with UUID [%s].", volumeUuid));
+        }
+
+        String volumeToString = 
ReflectionToStringBuilderUtils.reflectOnlySelectedFields(volume, "name", 
"uuid");
+
+        if (volume.getInstanceId() != null) {
+            VMInstanceVO vmInstanceVo = 
_vmInstanceDao.findById(volume.getInstanceId());
+            String msg = String.format("Volume [%s] is attached to [%s], so it 
cannot be moved to a different account.", volumeToString, vmInstanceVo);
+            s_logger.error(msg);
+            throw new PermissionDeniedException(msg);
+        }
+
+        List<SnapshotVO> snapshots = 
_snapshotDao.listByStatusNotIn(volume.getId(), Snapshot.State.Destroyed, 
Snapshot.State.Error);
+        if (CollectionUtils.isNotEmpty(snapshots)) {
+            throw new PermissionDeniedException(String.format("Volume [%s] has 
snapshots. Remove the volume's snapshots before assigning it to another 
account.", volumeToString));
+        }
+    }
+
+    protected Account getAccountOrProject(String projectUuid, Long accountId, 
Long projectId, Account caller) {
+        if (projectId != null && accountId != null) {
+            throw new InvalidParameterValueException("Both 'accountid' and 
'projectid' were informed. You must inform only one of them.");
+        }
+
+        if (projectId != null) {
+            Project project = projectManager.getProject(projectId);
+            if (project == null) {
+                throw new InvalidParameterValueException(String.format("Unable 
to find project [%s]", projectUuid));
+            }
+
+            if (!projectManager.canAccessProjectAccount(caller, 
project.getProjectAccountId())) {
+                throw new PermissionDeniedException(String.format("Account 
[%s] does not have access to project [%s].", caller, projectUuid));
+            }
+
+            return _accountMgr.getAccount(project.getProjectAccountId());
+        }
+
+        return  _accountMgr.getActiveAccountById(accountId);
+    }
+
     private Optional<String> 
setExtractVolumeSearchCriteria(SearchCriteria<VolumeDataStoreVO> sc, VolumeVO 
volume) {
         final long volumeId = volume.getId();
         sc.addAnd("state", SearchCriteria.Op.EQ, 
ObjectInDataStoreStateMachine.State.Ready.toString());
diff --git 
a/server/src/test/java/com/cloud/storage/VolumeApiServiceImplTest.java 
b/server/src/test/java/com/cloud/storage/VolumeApiServiceImplTest.java
index 45adc84d82a..e330997bcc8 100644
--- a/server/src/test/java/com/cloud/storage/VolumeApiServiceImplTest.java
+++ b/server/src/test/java/com/cloud/storage/VolumeApiServiceImplTest.java
@@ -17,6 +17,10 @@
 package com.cloud.storage;
 
 import static org.junit.Assert.assertEquals;
+import com.cloud.exception.PermissionDeniedException;
+import com.cloud.projects.Project;
+import com.cloud.projects.ProjectManager;
+import com.cloud.storage.dao.SnapshotDao;
 import static org.mockito.Matchers.any;
 import static org.mockito.Matchers.anyLong;
 import static org.mockito.Matchers.anyObject;
@@ -60,6 +64,8 @@ import 
org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao;
 import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
 import org.apache.cloudstack.storage.datastore.db.VolumeDataStoreDao;
 import org.apache.cloudstack.storage.datastore.db.VolumeDataStoreVO;
+import org.apache.cloudstack.utils.bytescale.ByteScaleUtils;
+import org.apache.commons.collections.CollectionUtils;
 import org.junit.After;
 import org.junit.Assert;
 import org.junit.Before;
@@ -191,12 +197,28 @@ public class VolumeApiServiceImplTest {
     private VolumeDataStoreVO volumeDataStoreVoMock;
     @Mock
     private AsyncCallFuture<VolumeApiResult> 
asyncCallFutureVolumeapiResultMock;
+    @Mock
+    private ArrayList<SnapshotVO> snapshotVOArrayListMock;
+
+    @Mock
+    private SnapshotDao snapshotDaoMock;
+
+    @Mock
+    private Project projectMock;
+
+    @Mock
+    private ProjectManager projectManagerMock;
 
     private long accountMockId = 456l;
     private long volumeMockId = 12313l;
     private long vmInstanceMockId = 1123l;
     private long volumeSizeMock = 456789921939l;
 
+    private String projectMockUuid = "projectUuid";
+    private long projecMockId = 13801801923810L;
+
+    private long projectMockAccountId = 132329390L;
+
     private long diskOfferingMockId = 100203L;
 
     private long offeringMockId = 31902L;
@@ -208,6 +230,9 @@ public class VolumeApiServiceImplTest {
         Mockito.lenient().doReturn(accountMockId).when(accountMock).getId();
         Mockito.doReturn(volumeSizeMock).when(volumeVoMock).getSize();
         
Mockito.doReturn(volumeSizeMock).when(newDiskOfferingMock).getDiskSize();
+        Mockito.doReturn(projectMockUuid).when(projectMock).getUuid();
+        Mockito.doReturn(projecMockId).when(projectMock).getId();
+        
Mockito.doReturn(projectMockAccountId).when(projectMock).getProjectAccountId();
 
         
Mockito.doReturn(Mockito.mock(VolumeApiResult.class)).when(asyncCallFutureVolumeapiResultMock).get();
 
@@ -1317,6 +1342,138 @@ public class VolumeApiServiceImplTest {
         Assert.assertEquals(expectedResult, result);
     }
 
+    @Test (expected = InvalidParameterValueException.class)
+    public void checkIfVolumeCanBeReassignedTestNullVolume() {
+        volumeApiServiceImpl.validateVolume(volumeVoMock.getUuid(), null);
+    }
+
+    @Test (expected = PermissionDeniedException.class)
+    public void checkIfVolumeCanBeReassignedTestAttachedVolume() {
+        Mockito.doReturn(vmInstanceMockId).when(volumeVoMock).getInstanceId();
+
+        volumeApiServiceImpl.validateVolume(volumeVoMock.getUuid(), 
volumeVoMock);
+    }
+
+    @Test (expected = PermissionDeniedException.class)
+    @PrepareForTest (CollectionUtils.class)
+    public void checkIfVolumeCanBeReassignedTestVolumeWithSnapshots() {
+        Mockito.doReturn(null).when(volumeVoMock).getInstanceId();
+        
Mockito.doReturn(snapshotVOArrayListMock).when(snapshotDaoMock).listByStatusNotIn(Mockito.anyLong(),
 Mockito.any(), Mockito.any());
+
+        PowerMockito.mockStatic(CollectionUtils.class);
+        
PowerMockito.when(CollectionUtils.isNotEmpty(snapshotVOArrayListMock)).thenReturn(true);
+
+        volumeApiServiceImpl.validateVolume(volumeVoMock.getUuid(), 
volumeVoMock);
+    }
+
+    @Test
+    @PrepareForTest (CollectionUtils.class)
+    public void checkIfVolumeCanBeReassignedTestValidVolume() {
+        Mockito.doReturn(null).when(volumeVoMock).getInstanceId();
+        
Mockito.doReturn(snapshotVOArrayListMock).when(snapshotDaoMock).listByStatusNotIn(Mockito.anyLong(),
 Mockito.any(), Mockito.any());
+
+        PowerMockito.mockStatic(CollectionUtils.class);
+        
PowerMockito.when(CollectionUtils.isNotEmpty(snapshotVOArrayListMock)).thenReturn(false);
+
+        volumeApiServiceImpl.validateVolume(volumeVoMock.getUuid(), 
volumeVoMock);
+    }
+
+    @Test (expected = InvalidParameterValueException.class)
+    public void validateAccountsTestNullOldAccount() {
+        volumeApiServiceImpl.validateAccounts(accountMock.getUuid(), 
volumeVoMock, null, accountMock);
+    }
+
+    @Test (expected = InvalidParameterValueException.class)
+    public void validateAccountsTestNullNewAccount() {
+        volumeApiServiceImpl.validateAccounts(accountMock.getUuid(), 
volumeVoMock, accountMock, null);
+    }
+
+    @Test (expected = InvalidParameterValueException.class)
+    public void validateAccountsTestDisabledNewAccount() {
+        Mockito.doReturn(Account.State.DISABLED).when(accountMock).getState();
+        volumeApiServiceImpl.validateAccounts(accountMock.getUuid(), 
volumeVoMock, null, accountMock);
+    }
+
+    @Test (expected = InvalidParameterValueException.class)
+    public void validateAccountsTestLockedNewAccount() {
+        Mockito.doReturn(Account.State.LOCKED).when(accountMock).getState();
+        volumeApiServiceImpl.validateAccounts(accountMock.getUuid(), 
volumeVoMock, null, accountMock);
+    }
+
+    @Test (expected = InvalidParameterValueException.class)
+    public void validateAccountsTestSameAccounts() {
+        volumeApiServiceImpl.validateAccounts(accountMock.getUuid(), 
volumeVoMock, accountMock, accountMock);
+    }
+
+    @Test
+    public void validateAccountsTestValidAccounts() {
+        Account newAccount = new AccountVO(accountMockId+1);
+        volumeApiServiceImpl.validateAccounts(accountMock.getUuid(), 
volumeVoMock, accountMock, newAccount);
+    }
+
+    @Test
+    @PrepareForTest(UsageEventUtils.class)
+    public void updateVolumeAccountTest() {
+        PowerMockito.mockStatic(UsageEventUtils.class);
+        Account newAccountMock = new AccountVO(accountMockId+1);
+
+        
Mockito.doReturn(volumeVoMock).when(volumeDaoMock).persist(volumeVoMock);
+
+        volumeApiServiceImpl.updateVolumeAccount(accountMock, volumeVoMock, 
newAccountMock);
+
+        PowerMockito.verifyStatic(UsageEventUtils.class);
+        UsageEventUtils.publishUsageEvent(EventTypes.EVENT_VOLUME_DELETE, 
volumeVoMock.getAccountId(), volumeVoMock.getDataCenterId(), 
volumeVoMock.getId(),
+                volumeVoMock.getName(), Volume.class.getName(), 
volumeVoMock.getUuid(), volumeVoMock.isDisplayVolume());
+
+        
Mockito.verify(resourceLimitServiceMock).decrementResourceCount(accountMock.getAccountId(),
 ResourceType.volume, ByteScaleUtils.bytesToGibibytes(volumeVoMock.getSize()));
+        
Mockito.verify(resourceLimitServiceMock).decrementResourceCount(accountMock.getAccountId(),
 ResourceType.primary_storage, volumeVoMock.getSize());
+
+        
Mockito.verify(volumeVoMock).setAccountId(newAccountMock.getAccountId());
+        Mockito.verify(volumeVoMock).setDomainId(newAccountMock.getDomainId());
+
+        Mockito.verify(volumeDaoMock).persist(volumeVoMock);
+
+        
Mockito.verify(resourceLimitServiceMock).incrementResourceCount(newAccountMock.getAccountId(),
 ResourceType.volume, ByteScaleUtils.bytesToGibibytes(volumeVoMock.getSize()));
+        
Mockito.verify(resourceLimitServiceMock).incrementResourceCount(newAccountMock.getAccountId(),
 ResourceType.primary_storage, volumeVoMock.getSize());
+
+        PowerMockito.verifyStatic(UsageEventUtils.class);
+        UsageEventUtils.publishUsageEvent(EventTypes.EVENT_VOLUME_DELETE, 
volumeVoMock.getAccountId(), volumeVoMock.getDataCenterId(), 
volumeVoMock.getId(),
+                volumeVoMock.getName(), Volume.class.getName(), 
volumeVoMock.getUuid(), volumeVoMock.isDisplayVolume());
+
+        
Mockito.verify(volumeServiceMock).moveVolumeOnSecondaryStorageToAnotherAccount(volumeVoMock,
 accountMock, newAccountMock);
+    }
+
+
+    @Test (expected = InvalidParameterValueException.class)
+    public void getAccountOrProjectTestAccountAndProjectInformed() {
+        volumeApiServiceImpl.getAccountOrProject(projectMock.getUuid(), 
accountMock.getId(), projectMock.getId(), accountMock);
+    }
+
+    @Test (expected = InvalidParameterValueException.class)
+    public void getAccountOrProjectTestUnableToFindProject() {
+        
Mockito.doReturn(null).when(projectManagerMock).getProject(projecMockId);
+        volumeApiServiceImpl.getAccountOrProject(projectMock.getUuid(), null, 
projectMock.getId(), accountMock);
+    }
+
+    @Test (expected = PermissionDeniedException.class)
+    public void getAccountOrProjectTestCallerDoesNotHaveAccessToProject() {
+        
Mockito.doReturn(projectMock).when(projectManagerMock).getProject(projecMockId);
+        
Mockito.doReturn(false).when(projectManagerMock).canAccessProjectAccount(accountMock,
 projectMockAccountId);
+        volumeApiServiceImpl.getAccountOrProject(projectMock.getUuid(), null, 
projectMock.getId(), accountMock);
+    }
+
+    @Test
+    public void getAccountOrProjectTestValidProject() {
+        
Mockito.doReturn(projectMock).when(projectManagerMock).getProject(projecMockId);
+        
Mockito.doReturn(true).when(projectManagerMock).canAccessProjectAccount(accountMock,
 projectMockAccountId);
+        volumeApiServiceImpl.getAccountOrProject(projectMock.getUuid(), null, 
projectMock.getId(), accountMock);
+    }
+
+    @Test
+    public void getAccountOrProjectTestValidAccount() {
+        volumeApiServiceImpl.getAccountOrProject(projectMock.getUuid(), 
accountMock.getId(),null, accountMock);
+    }
+
     @Test
     @PrepareForTest(UsageEventUtils.class)
     public void publishVolumeCreationUsageEventTestNullDiskOfferingId() {
diff --git 
a/services/secondary-storage/server/src/main/java/org/apache/cloudstack/storage/resource/NfsSecondaryStorageResource.java
 
b/services/secondary-storage/server/src/main/java/org/apache/cloudstack/storage/resource/NfsSecondaryStorageResource.java
index 6ad6e5fcda5..e32e2455e09 100644
--- 
a/services/secondary-storage/server/src/main/java/org/apache/cloudstack/storage/resource/NfsSecondaryStorageResource.java
+++ 
b/services/secondary-storage/server/src/main/java/org/apache/cloudstack/storage/resource/NfsSecondaryStorageResource.java
@@ -41,6 +41,7 @@ import java.net.URI;
 import java.net.UnknownHostException;
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.security.NoSuchAlgorithmException;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -58,6 +59,7 @@ import org.apache.cloudstack.storage.command.CopyCommand;
 import org.apache.cloudstack.storage.command.DeleteCommand;
 import org.apache.cloudstack.storage.command.DownloadCommand;
 import org.apache.cloudstack.storage.command.DownloadProgressCommand;
+import org.apache.cloudstack.storage.command.MoveVolumeCommand;
 import org.apache.cloudstack.storage.command.TemplateOrVolumePostUploadCommand;
 import org.apache.cloudstack.storage.command.UploadStatusAnswer;
 import org.apache.cloudstack.storage.command.UploadStatusAnswer.UploadStatus;
@@ -73,6 +75,7 @@ import org.apache.cloudstack.storage.to.SnapshotObjectTO;
 import org.apache.cloudstack.storage.to.TemplateObjectTO;
 import org.apache.cloudstack.storage.to.VolumeObjectTO;
 import org.apache.cloudstack.utils.imagestore.ImageStoreUtil;
+import 
org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils;
 import org.apache.cloudstack.utils.security.DigestHelper;
 import org.apache.commons.codec.digest.DigestUtils;
 import org.apache.commons.io.FileUtils;
@@ -311,6 +314,8 @@ public class NfsSecondaryStorageResource extends 
ServerResourceBase implements S
             return execute((GetDatadisksCommand)cmd);
         } else if (cmd instanceof CreateDatadiskTemplateCommand) {
             return execute((CreateDatadiskTemplateCommand)cmd);
+        } else if (cmd instanceof MoveVolumeCommand) {
+            return execute((MoveVolumeCommand)cmd);
         } else {
             return Answer.createUnsupportedCommandAnswer(cmd);
         }
@@ -544,6 +549,36 @@ public class NfsSecondaryStorageResource extends 
ServerResourceBase implements S
         return new CreateDatadiskTemplateAnswer(diskTemplate);
     }
 
+    public Answer execute(MoveVolumeCommand cmd) {
+        String volumeToString = 
ReflectionToStringBuilderUtils.reflectOnlySelectedFields(cmd, "volumeUuid", 
"volumeName");
+
+        String rootDir = getRootDir(cmd.getDatastoreUri(), _nfsVersion);
+
+        if (!rootDir.endsWith("/")) {
+            rootDir += "/";
+        }
+
+        Path srcPath = Paths.get(rootDir + cmd.getSrcPath());
+        Path destPath = Paths.get(rootDir + cmd.getDestPath());
+
+        try {
+            s_logger.debug(String.format("Trying to create missing directories 
(if any) to move volume [%s].", volumeToString));
+            Files.createDirectories(destPath.getParent());
+            s_logger.debug(String.format("Trying to move volume [%s] to 
[%s].", volumeToString, destPath));
+            Files.move(srcPath, destPath);
+
+            String msg = String.format("Moved volume [%s] from [%s] to [%s].", 
volumeToString, srcPath, destPath);
+            s_logger.debug(msg);
+
+            return new Answer(cmd, true, msg);
+
+        } catch (IOException ioException) {
+            s_logger.error(String.format("Failed to move volume [%s] from [%s] 
to [%s] due to [%s].", volumeToString, srcPath, destPath, 
ioException.getMessage()),
+                    ioException);
+            return new Answer(cmd, ioException);
+        }
+    }
+
     /*
      *  return Pair of <Template relative path, Template name>
      *  Template url may or may not end with .ova extension
diff --git 
a/utils/src/main/java/org/apache/cloudstack/utils/bytescale/ByteScaleUtils.java 
b/utils/src/main/java/org/apache/cloudstack/utils/bytescale/ByteScaleUtils.java
index 97043dbe82f..be3dd69c053 100644
--- 
a/utils/src/main/java/org/apache/cloudstack/utils/bytescale/ByteScaleUtils.java
+++ 
b/utils/src/main/java/org/apache/cloudstack/utils/bytescale/ByteScaleUtils.java
@@ -55,4 +55,14 @@ public class ByteScaleUtils {
     public static long bytesToMebibytes(long bytes) {
         return bytes / MiB;
     }
+
+    /**
+     * Converts bytes to gibibytes.
+     *
+     * @param b The value in bytes to convert to gibibytes.
+     * @return The parameter divided by 1024 * 1024 * 1024 (1 GiB).
+     */
+    public static long bytesToGibibytes(long b) {
+        return b / GiB;
+    }
 }
diff --git 
a/utils/src/test/java/org/apache/cloudstack/utils/bytescale/ByteScaleUtilsTest.java
 
b/utils/src/test/java/org/apache/cloudstack/utils/bytescale/ByteScaleUtilsTest.java
index dd3b577148b..6b056887d6e 100644
--- 
a/utils/src/test/java/org/apache/cloudstack/utils/bytescale/ByteScaleUtilsTest.java
+++ 
b/utils/src/test/java/org/apache/cloudstack/utils/bytescale/ByteScaleUtilsTest.java
@@ -43,6 +43,13 @@ public class ByteScaleUtilsTest {
         Assert.assertEquals(mib, ByteScaleUtils.bytesToMebibytes(bytes));
     }
 
+    @Test
+    public void validateBytesToGib(){
+        long gib = 3000L;
+        long b = 1024L * 1024L * 1024L * gib;
+        Assert.assertEquals(gib, ByteScaleUtils.bytesToGibibytes(b));
+    }
+
     @Test
     public void 
validateMebibytesToBytesIfIntTimesIntThenMustExtrapolateIntMaxValue() {
         int mib = 3000;


Reply via email to