This is an automated email from the ASF dual-hosted git repository. harikrishna pushed a commit to branch CheckVolumeAPI in repository https://gitbox.apache.org/repos/asf/cloudstack.git
commit 91f10143fd84998aad14efa4c250ad81e54c50ca Author: Harikrishna Patnala <[email protected]> AuthorDate: Tue Jan 30 12:57:48 2024 +0530 Introduced a new API checkVolumeAndRepair that allows users or admins to check and repair if any leaks observed. Currently this is supported only for KVM --- api/src/main/java/com/cloud/event/EventTypes.java | 1 + .../java/com/cloud/storage/VolumeApiService.java | 4 + .../org/apache/cloudstack/api/ApiConstants.java | 5 + .../admin/volume/RecoverVolumeCmdByAdmin.java | 2 +- .../user/volume/CheckVolumeAndRepairCmd.java | 116 +++++++++++++++ .../cloudstack/api/response/VolumeResponse.java | 25 ++++ .../api/storage/CheckVolumeAndRepairAnswer.java | 76 ++++++++++ .../api/storage/CheckVolumeAndRepairCommand.java | 77 ++++++++++ .../subsystem/api/storage/VolumeService.java | 2 + .../com/cloud/vm/VmWorkCheckAndRepairVolume.java | 41 ++++++ .../storage/volume/VolumeServiceImpl.java | 30 ++++ .../LibvirtCheckVolumeAndRepairCommandWrapper.java | 116 +++++++++++++++ .../org/apache/cloudstack/utils/qemu/QemuImg.java | 39 +++++ .../kvm/storage/LibvirtStoragePoolTest.java | 3 + .../com/cloud/server/ManagementServerImpl.java | 2 + .../cloud/storage/CheckAndRepairVolumePayload.java | 58 ++++++++ .../com/cloud/storage/VolumeApiServiceImpl.java | 162 +++++++++++++++++++++ .../src/main/java/com/cloud/utils/StringUtils.java | 22 +++ 18 files changed, 780 insertions(+), 1 deletion(-) diff --git a/api/src/main/java/com/cloud/event/EventTypes.java b/api/src/main/java/com/cloud/event/EventTypes.java index 67fed5500ee..41817525df2 100644 --- a/api/src/main/java/com/cloud/event/EventTypes.java +++ b/api/src/main/java/com/cloud/event/EventTypes.java @@ -303,6 +303,7 @@ public class EventTypes { public static final String EVENT_VOLUME_CREATE = "VOLUME.CREATE"; public static final String EVENT_VOLUME_DELETE = "VOLUME.DELETE"; public static final String EVENT_VOLUME_ATTACH = "VOLUME.ATTACH"; + public static final String EVENT_VOLUME_CHECK = "VOLUME.CHECK"; public static final String EVENT_VOLUME_DETACH = "VOLUME.DETACH"; public static final String EVENT_VOLUME_EXTRACT = "VOLUME.EXTRACT"; public static final String EVENT_VOLUME_UPLOAD = "VOLUME.UPLOAD"; diff --git a/api/src/main/java/com/cloud/storage/VolumeApiService.java b/api/src/main/java/com/cloud/storage/VolumeApiService.java index 8d5f7892f10..652d4ab3cc8 100644 --- a/api/src/main/java/com/cloud/storage/VolumeApiService.java +++ b/api/src/main/java/com/cloud/storage/VolumeApiService.java @@ -22,9 +22,11 @@ import java.net.MalformedURLException; import java.util.List; import java.util.Map; +import com.cloud.utils.Pair; 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.CheckVolumeAndRepairCmd; import org.apache.cloudstack.api.command.user.volume.CreateVolumeCmd; import org.apache.cloudstack.api.command.user.volume.DetachVolumeCmd; import org.apache.cloudstack.api.command.user.volume.ExtractVolumeCmd; @@ -178,4 +180,6 @@ public interface VolumeApiService { void publishVolumeCreationUsageEvent(Volume volume); boolean stateTransitTo(Volume vol, Volume.Event event) throws NoTransitionException; + + Pair<String, String> checkAndRepairVolume(CheckVolumeAndRepairCmd cmd) throws ResourceAllocationException; } diff --git a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java index db0c5ce494c..e3616900a8b 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -379,6 +379,8 @@ public class ApiConstants { public static final String RECEIVED_BYTES = "receivedbytes"; public static final String RECONNECT = "reconnect"; public static final String RECOVER = "recover"; + + public static final String REPAIR = "repair"; public static final String REQUIRES_HVM = "requireshvm"; public static final String RESOURCE_NAME = "resourcename"; public static final String RESOURCE_TYPE = "resourcetype"; @@ -502,6 +504,9 @@ public class ApiConstants { public static final String IS_VOLATILE = "isvolatile"; public static final String VOLUME_ID = "volumeid"; public static final String VOLUMES = "volumes"; + public static final String VOLUME_CHECK_RESULT = "volumecheckresult"; + public static final String VOLUME_REPAIR_RESULT = "volumerepairresult"; + public static final String ZONE = "zone"; public static final String ZONE_ID = "zoneid"; public static final String ZONE_NAME = "zonename"; 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 index f51aeec9719..92c62fa8662 100644 --- 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 @@ -30,7 +30,7 @@ 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}, +@APICommand(name = "recoverVolume", description = "Recovers a Destroy volume, curr", responseObject = VolumeResponse.class, responseView = ResponseView.Full, entityType = {Volume.class}, since = "4.14.0", authorized = {RoleType.Admin}, requestHasSensitiveInfo = false, diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/volume/CheckVolumeAndRepairCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/volume/CheckVolumeAndRepairCmd.java new file mode 100644 index 00000000000..268ccdcc0a4 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/volume/CheckVolumeAndRepairCmd.java @@ -0,0 +1,116 @@ +// 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.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiCommandResourceType; +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 org.apache.log4j.Logger; + +import com.cloud.exception.ResourceAllocationException; +import com.cloud.storage.Volume; +import com.cloud.user.Account; +import com.cloud.utils.Pair; +import com.cloud.utils.StringUtils; + +@APICommand(name = "checkVolumeAndRepair", description = "Check the volume and repair if needed, this is currently supported for KVM only", responseObject = VolumeResponse.class, entityType = {Volume.class}, + since = "4.18.1", + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}, + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = true) +public class CheckVolumeAndRepairCmd extends BaseCmd { + public static final Logger s_logger = Logger.getLogger(CheckVolumeAndRepairCmd.class.getName()); + + private static final String s_name = "checkvolumeandrepairresponse"; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = VolumeResponse.class, required = true, description = "The ID of the volume") + private Long id; + + @Parameter(name = ApiConstants.REPAIR, type = CommandType.BOOLEAN, required = false, description = "true if the volume has leaks and repair the volume") + private Boolean repair; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public Long getId() { + return id; + } + + public boolean getRepair() { + return repair == null ? false : repair; + } + + ///////////////////////////////////////////////////// + /////////////// 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 Long getApiResourceId() { + return id; + } + + @Override + public ApiCommandResourceType getApiResourceType() { + return ApiCommandResourceType.Volume; + } + + @Override + public void execute() throws ResourceAllocationException { + CallContext.current().setEventDetails("Volume Id: " + getId()); + Pair<String, String> result = _volumeService.checkAndRepairVolume(this); + Volume volume = _responseGenerator.findVolumeById(getId()); + if (result != null) { + VolumeResponse response = _responseGenerator.createVolumeResponse(ResponseView.Full, volume); + response.setVolumeCheckResult(StringUtils.parseJsonToMap(result.first())); + if (getRepair()) { + response.setVolumeRepairResult(StringUtils.parseJsonToMap(result.second())); + } + response.setResponseName(getCommandName()); + setResponseObject(response); + } else { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to check volume and repair"); + } + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/response/VolumeResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/VolumeResponse.java index 00a1eabc40b..785284ad46f 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/VolumeResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/VolumeResponse.java @@ -18,6 +18,7 @@ package org.apache.cloudstack.api.response; import java.util.Date; import java.util.LinkedHashSet; +import java.util.Map; import java.util.Set; import org.apache.cloudstack.acl.RoleType; @@ -288,6 +289,14 @@ public class VolumeResponse extends BaseResponseWithTagInformation implements Co @Param(description = "volume uuid that is given by virtualisation provider (only for VMware)") private String externalUuid; + @SerializedName(ApiConstants.VOLUME_CHECK_RESULT) + @Param(description = "details for the volume check result") + private Map<String, String> volumeCheckResult; + + @SerializedName(ApiConstants.VOLUME_REPAIR_RESULT) + @Param(description = "details for the volume repair result") + private Map<String, String> volumeRepairResult; + public String getPath() { return path; } @@ -817,4 +826,20 @@ public class VolumeResponse extends BaseResponseWithTagInformation implements Co public void setExternalUuid(String externalUuid) { this.externalUuid = externalUuid; } + + public Map<String, String> getVolumeCheckResult() { + return volumeCheckResult; + } + + public void setVolumeCheckResult(Map<String, String> volumeCheckResult) { + this.volumeCheckResult = volumeCheckResult; + } + + public Map<String, String> getVolumeRepairResult() { + return volumeRepairResult; + } + + public void setVolumeRepairResult(Map<String, String> volumeRepairResult) { + this.volumeRepairResult = volumeRepairResult; + } } diff --git a/core/src/main/java/com/cloud/agent/api/storage/CheckVolumeAndRepairAnswer.java b/core/src/main/java/com/cloud/agent/api/storage/CheckVolumeAndRepairAnswer.java new file mode 100644 index 00000000000..526e8933406 --- /dev/null +++ b/core/src/main/java/com/cloud/agent/api/storage/CheckVolumeAndRepairAnswer.java @@ -0,0 +1,76 @@ +// +// 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 com.cloud.agent.api.storage; + +import com.cloud.agent.api.Answer; + +public class CheckVolumeAndRepairAnswer extends Answer { + private long leaks; + private boolean repaired; + private long leaksFixed; + private String volumeCheckExecutionResult; + private String volumeRepairedExecutionResult; + + protected CheckVolumeAndRepairAnswer() { + super(); + } + + public CheckVolumeAndRepairAnswer(CheckVolumeAndRepairCommand cmd, boolean result, String details, long leaks, + boolean repaired, long leaksFixed, String volumeCheckExecutionResult, String volumeRepairedExecutionResult) { + super(cmd, result, details); + this.leaks = leaks; + this.repaired = repaired; + this.leaksFixed = leaksFixed; + this.volumeCheckExecutionResult = volumeCheckExecutionResult; + this.volumeRepairedExecutionResult = volumeRepairedExecutionResult; + } + + public CheckVolumeAndRepairAnswer(CheckVolumeAndRepairCommand cmd, boolean result, String details) { + super(cmd, result, details); + } + + public long getLeaks() { + return leaks; + } + + public boolean isRepaired() { + return repaired; + } + + public long getLeaksFixed() { + return leaksFixed; + } + + public String getVolumeCheckExecutionResult() { + return volumeCheckExecutionResult; + } + + public String getVolumeRepairedExecutionResult() { + return volumeRepairedExecutionResult; + } + + public void setVolumeCheckExecutionResult(String volumeCheckExecutionResult) { + this.volumeCheckExecutionResult = volumeCheckExecutionResult; + } + + public void setVolumeRepairedExecutionResult(String volumeRepairedExecutionResult) { + this.volumeRepairedExecutionResult = volumeRepairedExecutionResult; + } +} diff --git a/core/src/main/java/com/cloud/agent/api/storage/CheckVolumeAndRepairCommand.java b/core/src/main/java/com/cloud/agent/api/storage/CheckVolumeAndRepairCommand.java new file mode 100644 index 00000000000..040e6ca8608 --- /dev/null +++ b/core/src/main/java/com/cloud/agent/api/storage/CheckVolumeAndRepairCommand.java @@ -0,0 +1,77 @@ +// +// 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 com.cloud.agent.api.storage; + +import com.cloud.agent.api.Command; +import com.cloud.agent.api.LogLevel; +import com.cloud.agent.api.to.StorageFilerTO; + +import java.util.Arrays; + +public class CheckVolumeAndRepairCommand extends Command { + private String path; + private StorageFilerTO pool; + private boolean repair; + @LogLevel(LogLevel.Log4jLevel.Off) + private byte[] passphrase; + private String encryptFormat; + + public CheckVolumeAndRepairCommand(String path, StorageFilerTO pool, boolean repair, byte[] passphrase, String encryptFormat) { + this.path = path; + this.pool = pool; + this.repair = repair; + this.passphrase = passphrase; + this.encryptFormat = encryptFormat; + } + + public String getPath() { + return path; + } + + public String getPoolUuid() { + return pool.getUuid(); + } + + public StorageFilerTO getPool() { + return pool; + } + + public boolean needRepair() { + return repair; + } + + public String getEncryptFormat() { return encryptFormat; } + + public byte[] getPassphrase() { return passphrase; } + + public void clearPassphrase() { + if (this.passphrase != null) { + Arrays.fill(this.passphrase, (byte) 0); + } + } + + /** + * {@inheritDoc} + */ + @Override + public boolean executeInSequence() { + return false; + } +} 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 50aee83f497..4350e264125 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 @@ -115,4 +115,6 @@ public interface VolumeService { VolumeInfo sourceVolume, VolumeInfo destinationVolume, boolean retryExpungeVolumeAsync); void moveVolumeOnSecondaryStorageToAnotherAccount(Volume volume, Account sourceAccount, Account destAccount); + + Pair<String, String> checkAndRepairVolume(VolumeInfo volume); } diff --git a/engine/components-api/src/main/java/com/cloud/vm/VmWorkCheckAndRepairVolume.java b/engine/components-api/src/main/java/com/cloud/vm/VmWorkCheckAndRepairVolume.java new file mode 100644 index 00000000000..f4053dd1741 --- /dev/null +++ b/engine/components-api/src/main/java/com/cloud/vm/VmWorkCheckAndRepairVolume.java @@ -0,0 +1,41 @@ +// 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 com.cloud.vm; + +public class VmWorkCheckAndRepairVolume extends VmWork { + + private static final long serialVersionUID = 341816293003023823L; + + private Long volumeId; + + private boolean repair; + + public VmWorkCheckAndRepairVolume(long userId, long accountId, long vmId, String handlerName, + Long volumeId, boolean repair) { + super(userId, accountId, vmId, handlerName); + this.repair = repair; + this.volumeId = volumeId; + } + + public Long getVolumeId() { + return volumeId; + } + + public boolean needRepair() { + return repair; + } +} 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 8a3cd39ecbf..8c839a9b4d4 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 @@ -2760,6 +2760,36 @@ public class VolumeServiceImpl implements VolumeService { return snapshot; } + @Override + public Pair<String, String> checkAndRepairVolume(VolumeInfo volume) { + Long poolId = volume.getPoolId(); + StoragePool pool = _storageMgr.getStoragePool(poolId); + CheckAndRepairVolumePayload payload = (CheckAndRepairVolumePayload) volume.getpayload(); + CheckVolumeAndRepairCommand command = new CheckVolumeAndRepairCommand(volume.getPath(), new StorageFilerTO(pool), payload.isRepair(), + volume.getPassphrase(), volume.getEncryptFormat()); + + try { + CheckVolumeAndRepairAnswer answer = (CheckVolumeAndRepairAnswer) _storageMgr.sendToPool(pool, null, command); + if (answer != null && answer.getResult()) { + s_logger.debug("Check volume response result: " + answer.getDetails()); + payload.setVolumeCheckExecutionResult(answer.getVolumeCheckExecutionResult()); + if (payload.isRepair()) { + payload.setVolumeRepairedExecutionResult(answer.getVolumeRepairedExecutionResult()); + } + return new Pair<>(answer.getVolumeCheckExecutionResult(), answer.getVolumeRepairedExecutionResult()); + } else { + s_logger.debug("Failed to check and repair the volume with error " + answer.getDetails()); + } + + } catch (Exception e) { + s_logger.debug("sending check and repair volume command failed", e); + } finally { + command.clearPassphrase(); + } + + return null; + } + // For managed storage on Xen and VMware, we need to potentially make space for hypervisor snapshots. // The disk offering can collect this information and pass it on to the volume that's about to be created. // Ex. if you want a 10 GB CloudStack volume to reside on managed storage on Xen, this leads to an SR diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCheckVolumeAndRepairCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCheckVolumeAndRepairCommandWrapper.java new file mode 100644 index 00000000000..553de2953c1 --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCheckVolumeAndRepairCommandWrapper.java @@ -0,0 +1,116 @@ +// +// 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 com.cloud.hypervisor.kvm.resource.wrapper; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.storage.CheckVolumeAndRepairCommand; +import com.cloud.agent.api.storage.CheckVolumeAndRepairAnswer; +import com.cloud.agent.api.to.StorageFilerTO; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.hypervisor.kvm.storage.KVMPhysicalDisk; +import com.cloud.hypervisor.kvm.storage.KVMStoragePool; +import com.cloud.hypervisor.kvm.storage.KVMStoragePoolManager; +import com.cloud.storage.Storage; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ResourceWrapper; +import com.cloud.utils.exception.CloudRuntimeException; +import org.apache.cloudstack.utils.cryptsetup.KeyFile; +import org.apache.cloudstack.utils.qemu.QemuImageOptions; +import org.apache.cloudstack.utils.qemu.QemuImg; +import org.apache.cloudstack.utils.qemu.QemuImgException; +import org.apache.cloudstack.utils.qemu.QemuImgFile; +import org.apache.cloudstack.utils.qemu.QemuObject; +import org.apache.commons.lang.ArrayUtils; +import org.apache.log4j.Logger; +import org.libvirt.LibvirtException; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +@ResourceWrapper(handles = CheckVolumeAndRepairCommand.class) +public final class LibvirtCheckVolumeAndRepairCommandWrapper extends CommandWrapper<CheckVolumeAndRepairCommand, Answer, LibvirtComputingResource> { + + private static final Logger s_logger = Logger.getLogger(LibvirtCheckVolumeAndRepairCommandWrapper.class); + + @Override + public Answer execute(CheckVolumeAndRepairCommand command, LibvirtComputingResource serverResource) { + final String volumeId = command.getPath(); + final boolean repair = command.needRepair(); + final StorageFilerTO spool = command.getPool(); + + final KVMStoragePoolManager storagePoolMgr = serverResource.getStoragePoolMgr(); + KVMStoragePool pool = storagePoolMgr.getStoragePool(spool.getType(), spool.getUuid()); + + if (spool.getType().equals(Storage.StoragePoolType.PowerFlex)) { + pool.connectPhysicalDisk(volumeId, null); + } + + final KVMPhysicalDisk vol = pool.getPhysicalDisk(volumeId); + QemuObject.EncryptFormat encryptFormat = QemuObject.EncryptFormat.enumValue(command.getEncryptFormat()); + try { + String checkVolumeResult = checkVolumeAndRepair(vol, false, encryptFormat, command.getPassphrase(), serverResource); + s_logger.info(String.format("Check Volume result is %s", checkVolumeResult)); + CheckVolumeAndRepairAnswer answer = new CheckVolumeAndRepairAnswer(command, true, checkVolumeResult); + answer.setVolumeCheckExecutionResult(checkVolumeResult); + + if (repair) { + String repairVolumeResult = checkVolumeAndRepair(vol, true, encryptFormat, command.getPassphrase(), serverResource); + String finalResult = (checkVolumeResult != null ? checkVolumeResult.concat(",") : "") + repairVolumeResult; + s_logger.info(String.format("Repair Volume result for the volume %s is %s", vol.getName(), repairVolumeResult)); + + answer = new CheckVolumeAndRepairAnswer(command, true, finalResult); + answer.setVolumeRepairedExecutionResult(repairVolumeResult); + answer.setVolumeCheckExecutionResult(checkVolumeResult); + } + return answer; + } catch (Exception e) { + return new CheckVolumeAndRepairAnswer(command, false, e.toString()); + } + } + + private String checkVolumeAndRepair(final KVMPhysicalDisk vol, final boolean repair, final QemuObject.EncryptFormat encryptFormat, byte[] passphrase, final LibvirtComputingResource libvirtComputingResource) throws CloudRuntimeException { + List<QemuObject> passphraseObjects = new ArrayList<>(); + QemuImageOptions imgOptions = null; + if (ArrayUtils.isEmpty(passphrase)) { + passphrase = null; + } + try (KeyFile keyFile = new KeyFile(passphrase)) { + if (passphrase != null) { + passphraseObjects.add( + QemuObject.prepareSecretForQemuImg(vol.getFormat(), encryptFormat, keyFile.toString(), "sec0", null) + ); + imgOptions = new QemuImageOptions(vol.getFormat(), vol.getPath(),"sec0"); + } + QemuImg q = new QemuImg(libvirtComputingResource.getCmdsTimeout()); + QemuImgFile file = new QemuImgFile(vol.getPath()); + return q.checkAndRepair(file, imgOptions, passphraseObjects, repair); + } catch (QemuImgException | LibvirtException ex) { + throw new CloudRuntimeException("Failed to run qemu-img for check volume", ex); + } catch (IOException ex) { + throw new CloudRuntimeException("Failed to create keyfile for encrypted volume for check volume operation", ex); + } finally { + if (passphrase != null) { + Arrays.fill(passphrase, (byte) 0); + } + } + } +} diff --git a/plugins/hypervisors/kvm/src/main/java/org/apache/cloudstack/utils/qemu/QemuImg.java b/plugins/hypervisors/kvm/src/main/java/org/apache/cloudstack/utils/qemu/QemuImg.java index 1cd63b9b566..9a71a596420 100644 --- a/plugins/hypervisors/kvm/src/main/java/org/apache/cloudstack/utils/qemu/QemuImg.java +++ b/plugins/hypervisors/kvm/src/main/java/org/apache/cloudstack/utils/qemu/QemuImg.java @@ -812,4 +812,43 @@ public class QemuImg { Pattern pattern = Pattern.compile("Supported\\sformats:[a-zA-Z0-9-_\\s]*?\\b" + format + "\\b", CASE_INSENSITIVE); return pattern.matcher(text).find(); } + + /** + * check for any leaks for an image and repair. + * + * @param imageOptions + * Qemu style image options to be used in the checking process. + * @param qemuObjects + * Qemu style options (e.g. for passing secrets). + * @param repair + * Boolean option whether to repair any leaks + */ + public String checkAndRepair(final QemuImgFile file, final QemuImageOptions imageOptions, final List<QemuObject> qemuObjects, final boolean repair) throws QemuImgException { + final Script s = new Script(_qemuImgPath); + s.add("check"); + s.add(file.getFileName()); + + for (QemuObject o : qemuObjects) { + s.add(o.toCommandFlag()); + } + + if (imageOptions != null) { + s.add(imageOptions.toCommandFlag()); + } + + s.add("--output=json"); + + if (repair) { + s.add("-r"); + s.add("leaks"); + } + + OutputInterpreter.AllLinesParser parser = new OutputInterpreter.AllLinesParser(); + final String result = s.execute(parser); + if (result != null) { + throw new QemuImgException(result); + } + + return (parser.getLines() != null) ? parser.getLines().trim() : null; + } } diff --git a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/storage/LibvirtStoragePoolTest.java b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/storage/LibvirtStoragePoolTest.java index b2c58fd9b96..88d4daa2dab 100644 --- a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/storage/LibvirtStoragePoolTest.java +++ b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/storage/LibvirtStoragePoolTest.java @@ -87,6 +87,9 @@ public class LibvirtStoragePoolTest extends TestCase { StoragePool storage = Mockito.mock(StoragePool.class); LibvirtStoragePool nfsPool = new LibvirtStoragePool(uuid, name, StoragePoolType.NetworkFilesystem, adapter, storage); + if (nfsPool.getType() != StoragePoolType.NetworkFilesystem) { + System.out.println("tested"); + } assertFalse(nfsPool.isExternalSnapshot()); LibvirtStoragePool rbdPool = new LibvirtStoragePool(uuid, name, StoragePoolType.RBD, adapter, storage); diff --git a/server/src/main/java/com/cloud/server/ManagementServerImpl.java b/server/src/main/java/com/cloud/server/ManagementServerImpl.java index a73ba9b092c..56df8d501ee 100644 --- a/server/src/main/java/com/cloud/server/ManagementServerImpl.java +++ b/server/src/main/java/com/cloud/server/ManagementServerImpl.java @@ -553,6 +553,7 @@ 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.CheckVolumeAndRepairCmd; 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; @@ -3706,6 +3707,7 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe cmdList.add(ListVMGroupsCmd.class); cmdList.add(UpdateVMGroupCmd.class); cmdList.add(AttachVolumeCmd.class); + cmdList.add(CheckVolumeAndRepairCmd.class); cmdList.add(CreateVolumeCmd.class); cmdList.add(DeleteVolumeCmd.class); cmdList.add(UpdateVolumeCmd.class); diff --git a/server/src/main/java/com/cloud/storage/CheckAndRepairVolumePayload.java b/server/src/main/java/com/cloud/storage/CheckAndRepairVolumePayload.java new file mode 100644 index 00000000000..16a1531b1d6 --- /dev/null +++ b/server/src/main/java/com/cloud/storage/CheckAndRepairVolumePayload.java @@ -0,0 +1,58 @@ +// 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 com.cloud.storage; + +public class CheckAndRepairVolumePayload { + + public final boolean repair; + public String result; + private String volumeCheckExecutionResult; + private String volumeRepairedExecutionResult; + + public CheckAndRepairVolumePayload(boolean repair) { + this.repair = repair; + } + + public boolean isRepair() { + return repair; + } + + public String getResult() { + return result; + } + + public void setResult(String result) { + this.result = result; + } + + public String getVolumeCheckExecutionResult() { + return volumeCheckExecutionResult; + } + + public String getVolumeRepairedExecutionResult() { + return volumeRepairedExecutionResult; + } + + public void setVolumeCheckExecutionResult(String volumeCheckExecutionResult) { + this.volumeCheckExecutionResult = volumeCheckExecutionResult; + } + + public void setVolumeRepairedExecutionResult(String volumeRepairedExecutionResult) { + this.volumeRepairedExecutionResult = volumeRepairedExecutionResult; + } +} diff --git a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java index 2a0821c5c0a..a82cc1f1bd8 100644 --- a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java +++ b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java @@ -42,6 +42,7 @@ import org.apache.cloudstack.api.ServerApiException; 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.CheckVolumeAndRepairCmd; import org.apache.cloudstack.api.command.user.volume.CreateVolumeCmd; import org.apache.cloudstack.api.command.user.volume.DetachVolumeCmd; import org.apache.cloudstack.api.command.user.volume.ExtractVolumeCmd; @@ -217,6 +218,7 @@ import com.cloud.vm.VirtualMachineManager; import com.cloud.vm.VmDetailConstants; import com.cloud.vm.VmWork; import com.cloud.vm.VmWorkAttachVolume; +import com.cloud.vm.VmWorkCheckAndRepairVolume; import com.cloud.vm.VmWorkConstants; import com.cloud.vm.VmWorkDetachVolume; import com.cloud.vm.VmWorkExtractVolume; @@ -1817,7 +1819,142 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic s_logger.debug(String.format("Volume [%s] has been successfully recovered, thus a new usage event %s has been published.", volume.getUuid(), EventTypes.EVENT_VOLUME_CREATE)); } + @Override + @ActionEvent(eventType = EventTypes.EVENT_VOLUME_CHECK, eventDescription = "checking volume and repair if needed", async = true) + public Pair<String, String> checkAndRepairVolume(CheckVolumeAndRepairCmd cmd) throws ResourceAllocationException { + Account caller = CallContext.current().getCallingAccount(); + + // Verify input parameters + long volumeId = cmd.getId(); + boolean repair = cmd.getRepair(); + final VolumeVO volume = _volsDao.findById(volumeId); + + _accountMgr.checkAccess(caller, null, true, volume); + + Long vmId = volume.getInstanceId(); + UserVmVO vm = null; + if (vmId != null) { + vm = _userVmDao.findById(vmId); + if (vm == null) { + throw new InvalidParameterValueException(String.format("VM not found, please check the VM to which this volume %d is attached", volumeId)); + } + if (vm.getState() != State.Stopped) { + throw new InvalidParameterValueException(String.format("VM to which the volume %d is attached should be in stopped state", volumeId)); + } + } + + if (volume.getState() != Volume.State.Ready) { + throw new InvalidParameterValueException(String.format("VolumeId: %d is not in Ready state", volumeId)); + } + + HypervisorType hypervisorType = _volsDao.getHypervisorType(volume.getId()); + if (!HypervisorType.KVM.equals(hypervisorType)) { + throw new InvalidParameterValueException(String.format("Check and Repair volumes is supported only for KVM hypervisor")); + } + + if (vm != null) { + _accountMgr.checkAccess(caller, null, true, vm); + // serialize VM operation + AsyncJobExecutionContext jobContext = AsyncJobExecutionContext.getCurrentExecutionContext(); + if (jobContext.isJobDispatchedBy(VmWorkConstants.VM_WORK_JOB_DISPATCHER)) { + // avoid re-entrance + + VmWorkJobVO placeHolder = null; + placeHolder = createPlaceHolderWork(vm.getId()); + try { + Pair<String, String> result = orchestrateCheckVolumeAndRepair(volumeId, repair); + return result; + } finally { + _workJobDao.expunge(placeHolder.getId()); + } + } else { + Outcome<Pair> outcome = checkVolumeAndRepairThroughJobQueue(vm.getId(), volumeId, repair); + + try { + outcome.get(); + } catch (InterruptedException e) { + throw new RuntimeException("Operation is interrupted", e); + } catch (java.util.concurrent.ExecutionException e) { + throw new RuntimeException("Execution exception--", e); + } + + Object jobResult = _jobMgr.unmarshallResultObject(outcome.getJob()); + if (jobResult != null) { + if (jobResult instanceof ConcurrentOperationException) { + throw (ConcurrentOperationException)jobResult; + } else if (jobResult instanceof ResourceAllocationException) { + throw (ResourceAllocationException)jobResult; + } else if (jobResult instanceof Throwable) { + throw new RuntimeException("Unexpected exception", (Throwable)jobResult); + } + } + + // retrieve the entity url from job result + if (jobResult != null && jobResult instanceof Pair) { + return (Pair<String, String>) jobResult; + } + + return null; + } + } else { + CheckAndRepairVolumePayload payload = new CheckAndRepairVolumePayload(repair); + VolumeInfo volumeInfo = volFactory.getVolume(volumeId); + volumeInfo.addPayload(payload); + Pair<String, String> result = volService.checkAndRepairVolume(volumeInfo); + return result; + } + } + + private Pair<String, String> orchestrateCheckVolumeAndRepair(Long volumeId, boolean repair) { + + VolumeInfo volume = volFactory.getVolume(volumeId); + + if (volume == null) { + throw new InvalidParameterValueException("Checking volume and repairing failed due to volume:" + volumeId + " doesn't exist"); + } + + if (volume.getState() != Volume.State.Ready) { + throw new InvalidParameterValueException("VolumeId: " + volumeId + " is not in " + Volume.State.Ready + " state but " + volume.getState() + ". Cannot check and repair the volume."); + } + + CheckAndRepairVolumePayload payload = new CheckAndRepairVolumePayload(repair); + volume.addPayload(payload); + + return volService.checkAndRepairVolume(volume); + } + + public Outcome<Pair> checkVolumeAndRepairThroughJobQueue(final Long vmId, final Long volumeId, boolean repair) { + + final CallContext context = CallContext.current(); + final User callingUser = context.getCallingUser(); + final Account callingAccount = context.getCallingAccount(); + + final VMInstanceVO vm = _vmInstanceDao.findById(vmId); + + VmWorkJobVO workJob = new VmWorkJobVO(context.getContextId()); + + workJob.setDispatcher(VmWorkConstants.VM_WORK_JOB_DISPATCHER); + workJob.setCmd(VmWorkCheckAndRepairVolume.class.getName()); + + workJob.setAccountId(callingAccount.getId()); + workJob.setUserId(callingUser.getId()); + workJob.setStep(VmWorkJobVO.Step.Starting); + workJob.setVmType(VirtualMachine.Type.Instance); + workJob.setVmInstanceId(vm.getId()); + workJob.setRelated(AsyncJobExecutionContext.getOriginJobId()); + + // save work context info (there are some duplications) + VmWorkCheckAndRepairVolume workInfo = new VmWorkCheckAndRepairVolume(callingUser.getId(), callingAccount.getId(), vm.getId(), + VolumeApiServiceImpl.VM_WORK_JOB_HANDLER, volumeId, repair); + workJob.setCmdInfo(VmWorkSerializer.serialize(workInfo)); + + _jobMgr.submitAsyncJob(workJob, VmWorkConstants.VM_WORK_QUEUE, vm.getId()); + + AsyncJobExecutionContext.getCurrentExecutionContext().joinJob(workJob.getId()); + + return new VmJobCheckAndRepairVolumeOutcome(workJob); + } @Override @ActionEvent(eventType = EventTypes.EVENT_VOLUME_CHANGE_DISK_OFFERING, eventDescription = "Changing disk offering of a volume") @@ -4596,6 +4733,24 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic } } + public class VmJobCheckAndRepairVolumeOutcome extends OutcomeImpl<Pair> { + + public VmJobCheckAndRepairVolumeOutcome(final AsyncJob job) { + super(Pair.class, job, VmJobCheckInterval.value(), new Predicate() { + @Override + public boolean checkCondition() { + AsyncJobVO jobVo = _entityMgr.findById(AsyncJobVO.class, job.getId()); + assert (jobVo != null); + if (jobVo == null || jobVo.getStatus() != JobInfo.Status.IN_PROGRESS) { + return true; + } + + return false; + } + }, AsyncJob.Topics.JOB_STATE); + } + } + public Outcome<Volume> attachVolumeToVmThroughJobQueue(final Long vmId, final Long volumeId, final Long deviceId) { final CallContext context = CallContext.current(); @@ -4833,6 +4988,13 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic return new Pair<JobInfo.Status, String>(JobInfo.Status.SUCCEEDED, _jobMgr.marshallResultObject(work.getSnapshotId())); } + @ReflectionUse + private Pair<JobInfo.Status, String> orchestrateCheckVolumeAndRepair(VmWorkCheckAndRepairVolume work) throws Exception { + Account account = _accountDao.findById(work.getAccountId()); + Pair<String, String> result = orchestrateCheckVolumeAndRepair(work.getVolumeId(), work.needRepair()); + return new Pair<JobInfo.Status, String>(JobInfo.Status.SUCCEEDED, _jobMgr.marshallResultObject(result)); + } + @Override public Pair<JobInfo.Status, String> handleVmWorkJob(VmWork work) throws Exception { return _jobHandlerProxy.handleVmWorkJob(work); diff --git a/utils/src/main/java/com/cloud/utils/StringUtils.java b/utils/src/main/java/com/cloud/utils/StringUtils.java index 9e197a8a94b..ac9e6b09329 100644 --- a/utils/src/main/java/com/cloud/utils/StringUtils.java +++ b/utils/src/main/java/com/cloud/utils/StringUtils.java @@ -19,6 +19,10 @@ package com.cloud.utils; +import com.cloud.utils.exception.CloudRuntimeException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + import java.nio.charset.Charset; import java.util.ArrayList; import java.util.HashMap; @@ -282,4 +286,22 @@ public class StringUtils { final String value = keyValuePair.substring(index + 1); return new Pair<>(key.trim(), value.trim()); } + + public static Map<String, String> parseJsonToMap(String jsonString) { + ObjectMapper objectMapper = new ObjectMapper(); + Map<String, String> mapResult = new HashMap<>(); + + if (org.apache.commons.lang3.StringUtils.isNotEmpty(jsonString)) { + try { + JsonNode jsonNode = objectMapper.readTree(jsonString); + jsonNode.fields().forEachRemaining(entry -> { + mapResult.put(entry.getKey(), entry.getValue().asText()); + }); + } catch (Exception e) { + throw new CloudRuntimeException("Error while parsing json to convert it to map " + e.getMessage()); + } + } + + return mapResult; + } }
