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 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 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 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 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 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 setExtractVolumeSearchCriteria(SearchCriteria 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 asyncCallFutureVolumeapiResultMock; + @Mock + private ArrayList 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