This is an automated email from the ASF dual-hosted git repository.
rohit pushed a commit to branch 4.19
in repository https://gitbox.apache.org/repos/asf/cloudstack.git
The following commit(s) were added to refs/heads/4.19 by this push:
new c462be1412d New API "checkVolume" to check and repair any leaks or
issues reported by qemu-img check (#8577)
c462be1412d is described below
commit c462be1412d06bb81a8d9d2a1bd6718e20f5a477
Author: Harikrishna <[email protected]>
AuthorDate: Thu Feb 29 14:41:49 2024 +0530
New API "checkVolume" to check and repair any leaks or issues reported by
qemu-img check (#8577)
* 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
* some fixes
* Added unit tests
* addressed review comments
* add repair volume while granting access
* Changed repair parameter to accept both leaks/all
* Introduced new global setting volume.check.and.repair.before.use to do
volume check and repair before VM start or volume attach operations
* Added volume check and repair changes only during VM start and volume
attach operations
* Refactored the names to look similar across the code
* Some code fixes
* remove unused code
* Renamed repair values
* Fixed unit tests
* changed version
* Address review comments
* Code refactored
* used volume name in logs
* Changed the API to Async and the setting scope to storage pool
* Fixed exit value handling with check volume command
* Fixed storage scope to the setting
* Fix volume format issues
* Refactored the log messages
* Fix formatting
---
api/src/main/java/com/cloud/event/EventTypes.java | 1 +
.../java/com/cloud/storage/VolumeApiService.java | 4 +
.../org/apache/cloudstack/api/ApiConstants.java | 4 +
.../user/volume/CheckAndRepairVolumeCmd.java | 139 ++++++++++++++
.../cloudstack/api/response/VolumeResponse.java | 25 +++
.../api/storage/CheckAndRepairVolumeAnswer.java | 57 ++++++
.../api/storage/CheckAndRepairVolumeCommand.java | 77 ++++++++
.../subsystem/api/storage/VolumeService.java | 4 +
.../com/cloud/vm/VmWorkCheckAndRepairVolume.java | 42 +++++
.../engine/orchestration/VolumeOrchestrator.java | 12 ++
.../storage/volume/VolumeServiceImpl.java | 65 ++++++-
.../storage/volume/VolumeServiceTest.java | 93 ++++++++++
.../LibvirtCheckAndRepairVolumeCommandWrapper.java | 192 ++++++++++++++++++++
.../org/apache/cloudstack/utils/qemu/QemuImg.java | 41 +++++
...virtCheckAndRepairVolumeCommandWrapperTest.java | 98 ++++++++++
.../kvm/storage/LibvirtStoragePoolTest.java | 3 +
.../apache/cloudstack/utils/qemu/QemuImgTest.java | 17 ++
.../com/cloud/server/ManagementServerImpl.java | 2 +
.../cloud/storage/CheckAndRepairVolumePayload.java | 41 +++++
.../com/cloud/storage/VolumeApiServiceImpl.java | 202 ++++++++++++++++++++-
.../cloud/storage/VolumeApiServiceImplTest.java | 165 ++++++++++++++++-
.../src/main/java/com/cloud/utils/StringUtils.java | 22 +++
.../main/java/com/cloud/utils/script/Script.java | 132 ++++++++++++++
23 files changed, 1429 insertions(+), 9 deletions(-)
diff --git a/api/src/main/java/com/cloud/event/EventTypes.java
b/api/src/main/java/com/cloud/event/EventTypes.java
index 5d525229095..c4833d3433a 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..a673df12d0f 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.CheckAndRepairVolumeCmd;
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(CheckAndRepairVolumeCmd 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 3ae0f319189..18d25a0cfc3 100644
--- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java
+++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java
@@ -378,6 +378,7 @@ 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";
@@ -501,6 +502,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/user/volume/CheckAndRepairVolumeCmd.java
b/api/src/main/java/org/apache/cloudstack/api/command/user/volume/CheckAndRepairVolumeCmd.java
new file mode 100644
index 00000000000..9c0d1a1058a
--- /dev/null
+++
b/api/src/main/java/org/apache/cloudstack/api/command/user/volume/CheckAndRepairVolumeCmd.java
@@ -0,0 +1,139 @@
+// 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.event.EventTypes;
+import com.cloud.exception.InvalidParameterValueException;
+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.BaseAsyncCmd;
+import org.apache.cloudstack.api.Parameter;
+import org.apache.cloudstack.api.ResponseObject.ResponseView;
+import org.apache.cloudstack.api.ServerApiException;
+import org.apache.cloudstack.api.response.VolumeResponse;
+import org.apache.cloudstack.context.CallContext;
+import 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;
+
+import java.util.Arrays;
+
+@APICommand(name = "checkVolume", description = "Check the volume for any
errors or leaks and also repairs when repair parameter is passed, this is
currently supported for KVM only", responseObject = VolumeResponse.class,
entityType = {Volume.class},
+ since = "4.19.1",
+ authorized = {RoleType.Admin, RoleType.ResourceAdmin,
RoleType.DomainAdmin, RoleType.User})
+public class CheckAndRepairVolumeCmd extends BaseAsyncCmd {
+ public static final Logger s_logger =
Logger.getLogger(CheckAndRepairVolumeCmd.class.getName());
+
+ private static final String s_name = "checkandrepairvolumeresponse";
+
+ /////////////////////////////////////////////////////
+ //////////////// 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.STRING, required
= false, description = "parameter to repair the volume, leaks or all are the
possible values")
+ private String repair;
+
+ /////////////////////////////////////////////////////
+ /////////////////// Accessors ///////////////////////
+ /////////////////////////////////////////////////////
+
+ public enum RepairValues {
+ LEAKS, ALL
+ }
+
+ public Long getId() {
+ return id;
+ }
+
+ public String getRepair() {
+ if (org.apache.commons.lang3.StringUtils.isNotEmpty(repair)) {
+ RepairValues repairType = Enum.valueOf(RepairValues.class,
repair.toUpperCase());
+ if (repairType == null) {
+ throw new InvalidParameterValueException(String.format("Repair
parameter can only take the following values: %s" +
Arrays.toString(RepairValues.values())));
+ }
+ return repair.toLowerCase();
+ }
+ return null;
+ }
+
+ /////////////////////////////////////////////////////
+ /////////////// API Implementation///////////////////
+ /////////////////////////////////////////////////////
+
+ @Override
+ public String getCommandName() {
+ return s_name;
+ }
+
+ @Override
+ public long getEntityOwnerId() {
+ Volume volume = _entityMgr.findById(Volume.class, getId());
+ if (volume != null) {
+ return volume.getAccountId();
+ }
+
+ return Account.ACCOUNT_ID_SYSTEM; // no account info given, parent
this command to SYSTEM so ERROR events are tracked
+ }
+
+ @Override
+ public String getEventType() {
+ return EventTypes.EVENT_VOLUME_CHECK;
+ }
+
+ @Override
+ public String getEventDescription() {
+ return String.format("check and repair operation on volume: %s",
this._uuidMgr.getUuid(Volume.class, getId()));
+ }
+
+ @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() != null) {
+
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..0d502a6d7a7 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, they may vary
for different hypervisors, since = 4.19.1")
+ private Map<String, String> volumeCheckResult;
+
+ @SerializedName(ApiConstants.VOLUME_REPAIR_RESULT)
+ @Param(description = "details for the volume repair result, they may vary
for different hypervisors, since = 4.19.1")
+ 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/CheckAndRepairVolumeAnswer.java
b/core/src/main/java/com/cloud/agent/api/storage/CheckAndRepairVolumeAnswer.java
new file mode 100644
index 00000000000..3dc7752bfef
--- /dev/null
+++
b/core/src/main/java/com/cloud/agent/api/storage/CheckAndRepairVolumeAnswer.java
@@ -0,0 +1,57 @@
+//
+// 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 CheckAndRepairVolumeAnswer extends Answer {
+ private String volumeCheckExecutionResult;
+ private String volumeRepairExecutionResult;
+
+ protected CheckAndRepairVolumeAnswer() {
+ super();
+ }
+
+ public CheckAndRepairVolumeAnswer(CheckAndRepairVolumeCommand cmd, boolean
result, String details, String volumeCheckExecutionResult, String
volumeRepairedExecutionResult) {
+ super(cmd, result, details);
+ this.volumeCheckExecutionResult = volumeCheckExecutionResult;
+ this.volumeRepairExecutionResult = volumeRepairedExecutionResult;
+ }
+
+ public CheckAndRepairVolumeAnswer(CheckAndRepairVolumeCommand cmd, boolean
result, String details) {
+ super(cmd, result, details);
+ }
+
+ public String getVolumeCheckExecutionResult() {
+ return volumeCheckExecutionResult;
+ }
+
+ public String getVolumeRepairExecutionResult() {
+ return volumeRepairExecutionResult;
+ }
+
+ public void setVolumeCheckExecutionResult(String
volumeCheckExecutionResult) {
+ this.volumeCheckExecutionResult = volumeCheckExecutionResult;
+ }
+
+ public void setVolumeRepairExecutionResult(String
volumeRepairExecutionResult) {
+ this.volumeRepairExecutionResult = volumeRepairExecutionResult;
+ }
+}
diff --git
a/core/src/main/java/com/cloud/agent/api/storage/CheckAndRepairVolumeCommand.java
b/core/src/main/java/com/cloud/agent/api/storage/CheckAndRepairVolumeCommand.java
new file mode 100644
index 00000000000..2553fdf477c
--- /dev/null
+++
b/core/src/main/java/com/cloud/agent/api/storage/CheckAndRepairVolumeCommand.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 CheckAndRepairVolumeCommand extends Command {
+ private String path;
+ private StorageFilerTO pool;
+ private String repair;
+ @LogLevel(LogLevel.Log4jLevel.Off)
+ private byte[] passphrase;
+ private String encryptFormat;
+
+ public CheckAndRepairVolumeCommand(String path, StorageFilerTO pool,
String 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 String getRepair() {
+ 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 81bdf01d576..7c4d56e12b9 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
@@ -117,4 +117,8 @@ public interface VolumeService {
VolumeInfo sourceVolume, VolumeInfo destinationVolume, boolean
retryExpungeVolumeAsync);
void moveVolumeOnSecondaryStorageToAnotherAccount(Volume volume, Account
sourceAccount, Account destAccount);
+
+ Pair<String, String> checkAndRepairVolume(VolumeInfo volume);
+
+ void checkAndRepairVolumeBasedOnConfig(DataObject dataObject, Host host);
}
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..eaee4d19eb3
--- /dev/null
+++
b/engine/components-api/src/main/java/com/cloud/vm/VmWorkCheckAndRepairVolume.java
@@ -0,0 +1,42 @@
+// 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 = 341816293003023824L;
+
+ private Long volumeId;
+
+ private String repair;
+
+ public VmWorkCheckAndRepairVolume(long userId, long accountId, long vmId,
String handlerName,
+ Long volumeId, String repair) {
+ super(userId, accountId, vmId, handlerName);
+ this.repair = repair;
+ this.volumeId = volumeId;
+ }
+
+ public Long getVolumeId() {
+ return volumeId;
+ }
+
+ public String getRepair() {
+ return repair;
+ }
+}
diff --git
a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java
b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java
index cfcd129c79c..163631b3f4e 100644
---
a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java
+++
b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java
@@ -1915,6 +1915,8 @@ public class VolumeOrchestrator extends ManagerBase
implements VolumeOrchestrati
}
}
}
+ } else {
+ handleCheckAndRepairVolume(vol,
vm.getVirtualMachine().getHostId());
}
} else if (task.type == VolumeTaskType.MIGRATE) {
pool =
(StoragePool)dataStoreMgr.getDataStore(task.pool.getId(),
DataStoreRole.Primary);
@@ -1957,6 +1959,16 @@ public class VolumeOrchestrator extends ManagerBase
implements VolumeOrchestrati
}
}
+ private void handleCheckAndRepairVolume(Volume vol, Long hostId) {
+ Host host = _hostDao.findById(hostId);
+ try {
+
volService.checkAndRepairVolumeBasedOnConfig(volFactory.getVolume(vol.getId()),
host);
+ } catch (Exception e) {
+ String volumeToString = getReflectOnlySelectedFields(vol);
+ s_logger.debug(String.format("Unable to check and repair volume
[%s] on host [%s], due to %s.", volumeToString, host, e.getMessage()));
+ }
+ }
+
private boolean stateTransitTo(Volume vol, Volume.Event event) throws
NoTransitionException {
return _volStateMachine.transitTo(vol, event, null, _volsDao);
}
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 230978d399c..75f652da379 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
@@ -32,8 +32,10 @@ import java.util.concurrent.ExecutionException;
import javax.inject.Inject;
+import com.cloud.storage.VolumeApiServiceImpl;
import org.apache.cloudstack.annotation.AnnotationService;
import org.apache.cloudstack.annotation.dao.AnnotationDao;
+import org.apache.cloudstack.api.command.user.volume.CheckAndRepairVolumeCmd;
import org.apache.cloudstack.engine.cloud.entity.api.VolumeEntity;
import
org.apache.cloudstack.engine.orchestration.service.VolumeOrchestrationService;
import org.apache.cloudstack.engine.subsystem.api.storage.ChapInfo;
@@ -80,6 +82,7 @@ import
org.apache.cloudstack.storage.datastore.db.VolumeDataStoreVO;
import org.apache.cloudstack.storage.image.store.TemplateObject;
import org.apache.cloudstack.storage.to.TemplateObjectTO;
import org.apache.cloudstack.storage.to.VolumeObjectTO;
+import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.Logger;
import org.springframework.stereotype.Component;
@@ -87,9 +90,12 @@ import org.springframework.stereotype.Component;
import com.cloud.agent.AgentManager;
import com.cloud.agent.api.Answer;
import com.cloud.agent.api.ModifyTargetsCommand;
+import com.cloud.agent.api.storage.CheckAndRepairVolumeAnswer;
+import com.cloud.agent.api.storage.CheckAndRepairVolumeCommand;
import com.cloud.agent.api.storage.ListVolumeAnswer;
import com.cloud.agent.api.storage.ListVolumeCommand;
import com.cloud.agent.api.storage.ResizeVolumeCommand;
+import com.cloud.agent.api.to.DataObjectType;
import com.cloud.agent.api.to.StorageFilerTO;
import com.cloud.agent.api.to.VirtualMachineTO;
import com.cloud.alert.AlertManager;
@@ -110,6 +116,7 @@ import com.cloud.org.Cluster;
import com.cloud.org.Grouping.AllocationState;
import com.cloud.resource.ResourceState;
import com.cloud.server.ManagementService;
+import com.cloud.storage.CheckAndRepairVolumePayload;
import com.cloud.storage.DataStoreRole;
import com.cloud.storage.RegisterVolumePayload;
import com.cloud.storage.ScopeType;
@@ -198,7 +205,7 @@ public class VolumeServiceImpl implements VolumeService {
@Inject
private VolumeOrchestrationService _volumeMgr;
@Inject
- private StorageManager _storageMgr;
+ protected StorageManager _storageMgr;
@Inject
private AnnotationDao annotationDao;
@Inject
@@ -2773,6 +2780,62 @@ public class VolumeServiceImpl implements VolumeService {
return snapshot;
}
+ @Override
+ public void checkAndRepairVolumeBasedOnConfig(DataObject dataObject, Host
host) {
+ if (HypervisorType.KVM.equals(host.getHypervisorType()) &&
DataObjectType.VOLUME.equals(dataObject.getType())) {
+ VolumeInfo volumeInfo = volFactory.getVolume(dataObject.getId());
+ if
(VolumeApiServiceImpl.AllowCheckAndRepairVolume.valueIn(volumeInfo.getPoolId()))
{
+ s_logger.info(String.format("Trying to check and repair the
volume %d", dataObject.getId()));
+ String repair =
CheckAndRepairVolumeCmd.RepairValues.LEAKS.name().toLowerCase();
+ CheckAndRepairVolumePayload payload = new
CheckAndRepairVolumePayload(repair);
+ volumeInfo.addPayload(payload);
+ checkAndRepairVolumeThroughHost(volumeInfo, host);
+ }
+ }
+ }
+
+ @Override
+ public Pair<String, String> checkAndRepairVolume(VolumeInfo volume) {
+ Long poolId = volume.getPoolId();
+ List<Long> hostIds = _storageMgr.getUpHostsInPool(poolId);
+ if (CollectionUtils.isEmpty(hostIds)) {
+ throw new CloudRuntimeException("Unable to find Up hosts to run
the check volume command");
+ }
+ Collections.shuffle(hostIds);
+ Host host = _hostDao.findById(hostIds.get(0));
+
+ return checkAndRepairVolumeThroughHost(volume, host);
+
+ }
+
+ private Pair<String, String> checkAndRepairVolumeThroughHost(VolumeInfo
volume, Host host) {
+ Long poolId = volume.getPoolId();
+ StoragePool pool = _storageMgr.getStoragePool(poolId);
+ CheckAndRepairVolumePayload payload = (CheckAndRepairVolumePayload)
volume.getpayload();
+ CheckAndRepairVolumeCommand command = new
CheckAndRepairVolumeCommand(volume.getPath(), new StorageFilerTO(pool),
payload.getRepair(),
+ volume.getPassphrase(), volume.getEncryptFormat());
+
+ try {
+ grantAccess(volume, host, volume.getDataStore());
+ CheckAndRepairVolumeAnswer answer = (CheckAndRepairVolumeAnswer)
_storageMgr.sendToPool(pool, new long[]{host.getId()}, command);
+ if (answer != null && answer.getResult()) {
+ s_logger.debug(String.format("Check volume response result:
%s", answer.getDetails()));
+ return new Pair<>(answer.getVolumeCheckExecutionResult(),
answer.getVolumeRepairExecutionResult());
+ } else {
+ String errMsg = (answer == null) ? null : answer.getDetails();
+ s_logger.debug(String.format("Failed to check and repair the
volume with error %s", errMsg));
+ }
+
+ } catch (Exception e) {
+ s_logger.debug("sending check and repair volume command failed",
e);
+ } finally {
+ revokeAccess(volume, host, volume.getDataStore());
+ 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/engine/storage/volume/src/test/java/org/apache/cloudstack/storage/volume/VolumeServiceTest.java
b/engine/storage/volume/src/test/java/org/apache/cloudstack/storage/volume/VolumeServiceTest.java
index ee4b77c269c..55ff2f659af 100644
---
a/engine/storage/volume/src/test/java/org/apache/cloudstack/storage/volume/VolumeServiceTest.java
+++
b/engine/storage/volume/src/test/java/org/apache/cloudstack/storage/volume/VolumeServiceTest.java
@@ -19,6 +19,15 @@
package org.apache.cloudstack.storage.volume;
+import com.cloud.agent.api.storage.CheckAndRepairVolumeAnswer;
+import com.cloud.agent.api.storage.CheckAndRepairVolumeCommand;
+import com.cloud.agent.api.to.StorageFilerTO;
+import com.cloud.exception.StorageUnavailableException;
+import com.cloud.host.HostVO;
+import com.cloud.host.dao.HostDao;
+import com.cloud.storage.CheckAndRepairVolumePayload;
+import com.cloud.storage.StorageManager;
+import com.cloud.storage.StoragePool;
import com.cloud.storage.VolumeVO;
import com.cloud.storage.dao.VolumeDao;
import com.cloud.storage.snapshot.SnapshotManager;
@@ -26,6 +35,8 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ExecutionException;
+
+import com.cloud.utils.Pair;
import junit.framework.TestCase;
import
org.apache.cloudstack.engine.subsystem.api.storage.ObjectInDataStoreStateMachine;
import org.apache.cloudstack.engine.subsystem.api.storage.VolumeDataFactory;
@@ -65,15 +76,26 @@ public class VolumeServiceTest extends TestCase{
@Mock
SnapshotManager snapshotManagerMock;
+ @Mock
+ StorageManager storageManagerMock;
+
@Mock
VolumeVO volumeVoMock;
+ @Mock
+ HostVO hostMock;
+
+ @Mock
+ HostDao hostDaoMock;
+
@Before
public void setup(){
volumeServiceImplSpy = Mockito.spy(new VolumeServiceImpl());
volumeServiceImplSpy.volFactory = volumeDataFactoryMock;
volumeServiceImplSpy.volDao = volumeDaoMock;
volumeServiceImplSpy.snapshotMgr = snapshotManagerMock;
+ volumeServiceImplSpy._storageMgr = storageManagerMock;
+ volumeServiceImplSpy._hostDao = hostDaoMock;
}
@Test(expected = InterruptedException.class)
@@ -210,4 +232,75 @@ public class VolumeServiceTest extends TestCase{
volumeServiceImplSpy.destroySourceVolumeAfterMigration(ObjectInDataStoreStateMachine.Event.DestroyRequested,
null, volumeObject,
volumeObject, true);
}
+
+ @Test
+ public void testCheckAndRepairVolume() throws StorageUnavailableException {
+ VolumeInfo volume = Mockito.mock(VolumeInfo.class);
+ Mockito.when(volume.getPoolId()).thenReturn(1L);
+ StoragePool pool = Mockito.mock(StoragePool.class);
+ Mockito.when(storageManagerMock.getStoragePool(1L)).thenReturn(pool);
+ List<Long> hostIds = new ArrayList<>();
+ hostIds.add(1L);
+
Mockito.when(storageManagerMock.getUpHostsInPool(1L)).thenReturn(hostIds);
+ Mockito.when(hostMock.getId()).thenReturn(1L);
+ Mockito.when(hostDaoMock.findById(1L)).thenReturn(hostMock);
+
+ CheckAndRepairVolumePayload payload = new
CheckAndRepairVolumePayload(null);
+ Mockito.when(volume.getpayload()).thenReturn(payload);
+
Mockito.when(volume.getPath()).thenReturn("cbac516a-0f1f-4559-921c-1a7c6c408ccf");
+ Mockito.when(volume.getPassphrase()).thenReturn(new byte[] {3, 1, 2,
3});
+ Mockito.when(volume.getEncryptFormat()).thenReturn("LUKS");
+
+ String checkResult = "{\n" +
+ " \"image-end-offset\": 6442582016,\n" +
+ " \"total-clusters\": 163840,\n" +
+ " \"check-errors\": 0,\n" +
+ " \"leaks\": 124,\n" +
+ " \"allocated-clusters\": 98154,\n" +
+ " \"filename\":
\"/var/lib/libvirt/images/26be20c7-b9d0-43f6-a76e-16c70737a0e0\",\n" +
+ " \"format\": \"qcow2\",\n" +
+ " \"fragmented-clusters\": 96135\n" +
+ "}";
+
+ CheckAndRepairVolumeCommand command = new
CheckAndRepairVolumeCommand(volume.getPath(), new StorageFilerTO(pool),
payload.getRepair(),
+ volume.getPassphrase(), volume.getEncryptFormat());
+
+ CheckAndRepairVolumeAnswer answer = new
CheckAndRepairVolumeAnswer(command, true, checkResult);
+ answer.setVolumeCheckExecutionResult(checkResult);
+ Mockito.when(storageManagerMock.sendToPool(pool, new long[]{1L},
command)).thenReturn(answer);
+
+ Pair<String, String> result =
volumeServiceImplSpy.checkAndRepairVolume(volume);
+
+ Assert.assertEquals(result.first(), checkResult);
+ Assert.assertEquals(result.second(), null);
+ }
+
+ @Test
+ public void testCheckAndRepairVolumeWhenFailure() throws
StorageUnavailableException {
+ VolumeInfo volume = Mockito.mock(VolumeInfo.class);
+ Mockito.when(volume.getPoolId()).thenReturn(1L);
+ StoragePool pool = Mockito.mock(StoragePool.class);
+ Mockito.when(storageManagerMock.getStoragePool(1L)).thenReturn(pool);
+ List<Long> hostIds = new ArrayList<>();
+ hostIds.add(1L);
+
Mockito.when(storageManagerMock.getUpHostsInPool(1L)).thenReturn(hostIds);
+ Mockito.when(hostMock.getId()).thenReturn(1L);
+ Mockito.when(hostDaoMock.findById(1L)).thenReturn(hostMock);
+
+ CheckAndRepairVolumePayload payload = new
CheckAndRepairVolumePayload(null);
+ Mockito.when(volume.getpayload()).thenReturn(payload);
+
Mockito.when(volume.getPath()).thenReturn("cbac516a-0f1f-4559-921c-1a7c6c408ccf");
+ Mockito.when(volume.getPassphrase()).thenReturn(new byte[] {3, 1, 2,
3});
+ Mockito.when(volume.getEncryptFormat()).thenReturn("LUKS");
+
+ CheckAndRepairVolumeCommand command = new
CheckAndRepairVolumeCommand(volume.getPath(), new StorageFilerTO(pool),
payload.getRepair(),
+ volume.getPassphrase(), volume.getEncryptFormat());
+
+ CheckAndRepairVolumeAnswer answer = new
CheckAndRepairVolumeAnswer(command, false, "Unable to execute qemu command");
+ Mockito.when(storageManagerMock.sendToPool(pool, new long[]{1L},
command)).thenReturn(answer);
+
+ Pair<String, String> result =
volumeServiceImplSpy.checkAndRepairVolume(volume);
+
+ Assert.assertEquals(null, result);
+ }
}
diff --git
a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCheckAndRepairVolumeCommandWrapper.java
b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCheckAndRepairVolumeCommandWrapper.java
new file mode 100644
index 00000000000..cd81a2fbc23
--- /dev/null
+++
b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCheckAndRepairVolumeCommandWrapper.java
@@ -0,0 +1,192 @@
+//
+// 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.CheckAndRepairVolumeCommand;
+import com.cloud.agent.api.storage.CheckAndRepairVolumeAnswer;
+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.resource.CommandWrapper;
+import com.cloud.resource.ResourceWrapper;
+import com.cloud.utils.exception.CloudRuntimeException;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.JsonNode;
+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.cloudstack.utils.qemu.QemuObject.EncryptFormat;
+import org.apache.commons.lang.ArrayUtils;
+import org.apache.commons.lang3.StringUtils;
+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 = CheckAndRepairVolumeCommand.class)
+public class LibvirtCheckAndRepairVolumeCommandWrapper extends
CommandWrapper<CheckAndRepairVolumeCommand, Answer, LibvirtComputingResource> {
+
+ private static final Logger s_logger =
Logger.getLogger(LibvirtCheckAndRepairVolumeCommandWrapper.class);
+
+ @Override
+ public Answer execute(CheckAndRepairVolumeCommand command,
LibvirtComputingResource serverResource) {
+ final String volumeId = command.getPath();
+ final String repair = command.getRepair();
+ final StorageFilerTO spool = command.getPool();
+
+ final KVMStoragePoolManager storagePoolMgr =
serverResource.getStoragePoolMgr();
+ KVMStoragePool pool = storagePoolMgr.getStoragePool(spool.getType(),
spool.getUuid());
+ final KVMPhysicalDisk vol = pool.getPhysicalDisk(volumeId);
+ byte[] passphrase = command.getPassphrase();
+
+ try {
+ CheckAndRepairVolumeAnswer answer = null;
+ String checkVolumeResult = null;
+ if (QemuImg.PhysicalDiskFormat.RAW.equals(vol.getFormat())) {
+ checkVolumeResult = "Volume format RAW is not supported to
check and repair";
+ String jsonStringFormat = String.format("{ \"message\": \"%s\"
}", checkVolumeResult);
+ answer = new CheckAndRepairVolumeAnswer(command, true,
checkVolumeResult);
+ answer.setVolumeCheckExecutionResult(jsonStringFormat);
+
+ return answer;
+ } else {
+ answer = checkVolume(vol, command, serverResource);
+ checkVolumeResult = answer.getVolumeCheckExecutionResult();
+ }
+
+ CheckAndRepairVolumeAnswer resultAnswer =
checkIfRepairLeaksIsRequired(command, checkVolumeResult, vol.getName());
+ // resultAnswer is not null when repair is not required, so return
from here
+ if (resultAnswer != null) {
+ return resultAnswer;
+ }
+
+ if (StringUtils.isNotEmpty(repair)) {
+ answer = repairVolume(vol, command, serverResource,
checkVolumeResult);
+ }
+
+ return answer;
+ } catch (Exception e) {
+ return new CheckAndRepairVolumeAnswer(command, false,
e.toString());
+ } finally {
+ if (passphrase != null) {
+ Arrays.fill(passphrase, (byte) 0);
+ }
+ }
+ }
+
+ private CheckAndRepairVolumeAnswer checkVolume(KVMPhysicalDisk vol,
CheckAndRepairVolumeCommand command, LibvirtComputingResource serverResource) {
+ EncryptFormat encryptFormat =
EncryptFormat.enumValue(command.getEncryptFormat());
+ byte[] passphrase = command.getPassphrase();
+ String checkVolumeResult = checkAndRepairVolume(vol, null,
encryptFormat, passphrase, serverResource);
+ s_logger.info(String.format("Check Volume result for the volume %s is
%s", vol.getName(), checkVolumeResult));
+ CheckAndRepairVolumeAnswer answer = new
CheckAndRepairVolumeAnswer(command, true, checkVolumeResult);
+ answer.setVolumeCheckExecutionResult(checkVolumeResult);
+
+ return answer;
+ }
+
+ private CheckAndRepairVolumeAnswer repairVolume(KVMPhysicalDisk vol,
CheckAndRepairVolumeCommand command, LibvirtComputingResource serverResource,
String checkVolumeResult) {
+ EncryptFormat encryptFormat =
EncryptFormat.enumValue(command.getEncryptFormat());
+ byte[] passphrase = command.getPassphrase();
+ final String repair = command.getRepair();
+
+ String repairVolumeResult = checkAndRepairVolume(vol, repair,
encryptFormat, passphrase, 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));
+
+ CheckAndRepairVolumeAnswer answer = new
CheckAndRepairVolumeAnswer(command, true, finalResult);
+ answer.setVolumeRepairExecutionResult(repairVolumeResult);
+ answer.setVolumeCheckExecutionResult(checkVolumeResult);
+
+ return answer;
+ }
+
+ private CheckAndRepairVolumeAnswer
checkIfRepairLeaksIsRequired(CheckAndRepairVolumeCommand command, String
checkVolumeResult, String volumeName) {
+ final String repair = command.getRepair();
+ int leaks = 0;
+ if (StringUtils.isNotEmpty(checkVolumeResult) &&
StringUtils.isNotEmpty(repair) && repair.equals("leaks")) {
+ ObjectMapper objectMapper = new ObjectMapper();
+ JsonNode jsonNode = null;
+ try {
+ jsonNode = objectMapper.readTree(checkVolumeResult);
+ } catch (JsonProcessingException e) {
+ String msg = String.format("Error processing response %s
during check volume %s", checkVolumeResult, e.getMessage());
+ s_logger.info(msg);
+
+ return skipRepairVolumeCommand(command, checkVolumeResult,
msg);
+ }
+ JsonNode leaksNode = jsonNode.get("leaks");
+ if (leaksNode != null) {
+ leaks = leaksNode.asInt();
+ }
+
+ if (leaks == 0) {
+ String msg = String.format("No leaks found while checking for
the volume %s, so skipping repair", volumeName);
+ return skipRepairVolumeCommand(command, checkVolumeResult,
msg);
+ }
+ }
+
+ return null;
+ }
+
+ private CheckAndRepairVolumeAnswer
skipRepairVolumeCommand(CheckAndRepairVolumeCommand command, String
checkVolumeResult, String msg) {
+ s_logger.info(msg);
+ String jsonStringFormat = String.format("{ \"message\": \"%s\" }",
msg);
+ String finalResult = (checkVolumeResult != null ?
checkVolumeResult.concat(",") : "") + jsonStringFormat;
+ CheckAndRepairVolumeAnswer answer = new
CheckAndRepairVolumeAnswer(command, true, finalResult);
+ answer.setVolumeRepairExecutionResult(jsonStringFormat);
+ answer.setVolumeCheckExecutionResult(checkVolumeResult);
+
+ return answer;
+ }
+
+ protected String checkAndRepairVolume(final KVMPhysicalDisk vol, final
String repair, final 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);
+ }
+ }
+}
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..360c762deb0 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,45 @@ 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 String
repair) throws QemuImgException {
+ final Script script = new Script(_qemuImgPath);
+ script.add("check");
+ if (imageOptions == null) {
+ script.add(file.getFileName());
+ }
+
+ for (QemuObject o : qemuObjects) {
+ script.add(o.toCommandFlag());
+ }
+
+ if (imageOptions != null) {
+ script.add(imageOptions.toCommandFlag());
+ }
+
+ if (StringUtils.isNotEmpty(repair)) {
+ script.add("-r");
+ script.add(repair);
+ }
+
+ script.add("--output=json");
+ script.add("2>/dev/null");
+
+ final String result =
Script.runBashScriptIgnoreExitValue(script.toString(), 3);
+ if (result != null) {
+ logger.debug(String.format("Check volume execution result %s",
result));
+ }
+
+ return result;
+ }
}
diff --git
a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCheckAndRepairVolumeCommandWrapperTest.java
b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCheckAndRepairVolumeCommandWrapperTest.java
new file mode 100644
index 00000000000..e2120e46d13
--- /dev/null
+++
b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCheckAndRepairVolumeCommandWrapperTest.java
@@ -0,0 +1,98 @@
+/*
+ * Licensed 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.storage.CheckAndRepairVolumeAnswer;
+import com.cloud.agent.api.storage.CheckAndRepairVolumeCommand;
+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 org.apache.cloudstack.utils.qemu.QemuImg;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockedConstruction;
+import org.mockito.Mockito;
+import org.mockito.Spy;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import static org.mockito.Mockito.when;
+
+@RunWith(MockitoJUnitRunner.class)
+public class LibvirtCheckAndRepairVolumeCommandWrapperTest {
+
+ @Spy
+ LibvirtCheckAndRepairVolumeCommandWrapper
libvirtCheckAndRepairVolumeCommandWrapperSpy =
Mockito.spy(LibvirtCheckAndRepairVolumeCommandWrapper.class);
+
+ @Mock
+ LibvirtComputingResource libvirtComputingResourceMock;
+
+ @Mock
+ CheckAndRepairVolumeCommand checkAndRepairVolumeCommand;
+
+ @Mock
+ QemuImg qemuImgMock;
+
+ @Before
+ public void init() {
+ when(libvirtComputingResourceMock.getCmdsTimeout()).thenReturn(60);
+ }
+
+ @Test
+ public void testCheckAndRepairVolume() throws Exception {
+
+ CheckAndRepairVolumeCommand cmd =
Mockito.mock(CheckAndRepairVolumeCommand.class);
+ when(cmd.getPath()).thenReturn("cbac516a-0f1f-4559-921c-1a7c6c408ccf");
+ when(cmd.getRepair()).thenReturn(null);
+ StorageFilerTO spool = Mockito.mock(StorageFilerTO.class);
+ when(cmd.getPool()).thenReturn(spool);
+
+ KVMStoragePoolManager storagePoolMgr =
Mockito.mock(KVMStoragePoolManager.class);
+
when(libvirtComputingResourceMock.getStoragePoolMgr()).thenReturn(storagePoolMgr);
+ KVMStoragePool pool = Mockito.mock(KVMStoragePool.class);
+ when(spool.getType()).thenReturn(Storage.StoragePoolType.PowerFlex);
+
when(spool.getUuid()).thenReturn("b6be258b-42b8-49a4-ad51-3634ef8ff76a");
+ when(storagePoolMgr.getStoragePool(Storage.StoragePoolType.PowerFlex,
"b6be258b-42b8-49a4-ad51-3634ef8ff76a")).thenReturn(pool);
+
+ KVMPhysicalDisk vol = Mockito.mock(KVMPhysicalDisk.class);
+
when(pool.getPhysicalDisk("cbac516a-0f1f-4559-921c-1a7c6c408ccf")).thenReturn(vol);
+
Mockito.when(vol.getFormat()).thenReturn(QemuImg.PhysicalDiskFormat.QCOW2);
+
+ String checkResult = "{\n" +
+ " \"image-end-offset\": 6442582016,\n" +
+ " \"total-clusters\": 163840,\n" +
+ " \"check-errors\": 0,\n" +
+ " \"leaks\": 124,\n" +
+ " \"allocated-clusters\": 98154,\n" +
+ " \"filename\":
\"/var/lib/libvirt/images/26be20c7-b9d0-43f6-a76e-16c70737a0e0\",\n" +
+ " \"format\": \"qcow2\",\n" +
+ " \"fragmented-clusters\": 96135\n" +
+ "}";
+
+ try (MockedConstruction<QemuImg> ignored =
Mockito.mockConstruction(QemuImg.class, (mock, context) -> {
+ when(mock.checkAndRepair(Mockito.any(), Mockito.any(),
Mockito.any(), Mockito.any())).thenReturn(checkResult);
+ })) {
+ CheckAndRepairVolumeAnswer result = (CheckAndRepairVolumeAnswer)
libvirtCheckAndRepairVolumeCommandWrapperSpy.execute(cmd,
libvirtComputingResourceMock);
+ Assert.assertEquals(checkResult,
result.getVolumeCheckExecutionResult());
+ }
+ }
+
+}
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/plugins/hypervisors/kvm/src/test/java/org/apache/cloudstack/utils/qemu/QemuImgTest.java
b/plugins/hypervisors/kvm/src/test/java/org/apache/cloudstack/utils/qemu/QemuImgTest.java
index 8bb762cca85..b0981dde26e 100644
---
a/plugins/hypervisors/kvm/src/test/java/org/apache/cloudstack/utils/qemu/QemuImgTest.java
+++
b/plugins/hypervisors/kvm/src/test/java/org/apache/cloudstack/utils/qemu/QemuImgTest.java
@@ -368,4 +368,21 @@ public class QemuImgTest {
Assert.assertTrue("should support qcow2",
QemuImg.helpSupportsImageFormat(partialHelp, PhysicalDiskFormat.QCOW2));
Assert.assertFalse("should not support http",
QemuImg.helpSupportsImageFormat(partialHelp, PhysicalDiskFormat.SHEEPDOG));
}
+
+ @Test
+ public void testCheckAndRepair() throws LibvirtException {
+ String filename = "/tmp/" + UUID.randomUUID() + ".qcow2";
+
+ QemuImgFile file = new QemuImgFile(filename);
+
+ try {
+ QemuImg qemu = new QemuImg(0);
+ qemu.checkAndRepair(file, null, null, null);
+ } catch (QemuImgException e) {
+ fail(e.getMessage());
+ }
+
+ File f = new File(filename);
+ f.delete();
+ }
}
diff --git a/server/src/main/java/com/cloud/server/ManagementServerImpl.java
b/server/src/main/java/com/cloud/server/ManagementServerImpl.java
index 430222a78ac..a6935667b8c 100644
--- a/server/src/main/java/com/cloud/server/ManagementServerImpl.java
+++ b/server/src/main/java/com/cloud/server/ManagementServerImpl.java
@@ -552,6 +552,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.CheckAndRepairVolumeCmd;
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;
@@ -3704,6 +3705,7 @@ public class ManagementServerImpl extends ManagerBase
implements ManagementServe
cmdList.add(ListVMGroupsCmd.class);
cmdList.add(UpdateVMGroupCmd.class);
cmdList.add(AttachVolumeCmd.class);
+ cmdList.add(CheckAndRepairVolumeCmd.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..eabe1a4c7b8
--- /dev/null
+++ b/server/src/main/java/com/cloud/storage/CheckAndRepairVolumePayload.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.storage;
+
+public class CheckAndRepairVolumePayload {
+
+ public final String repair;
+ public String result;
+
+ public CheckAndRepairVolumePayload(String repair) {
+ this.repair = repair;
+ }
+
+ public String getRepair() {
+ return repair;
+ }
+
+ public String getResult() {
+ return result;
+ }
+
+ public void setResult(String result) {
+ this.result = result;
+ }
+
+}
diff --git a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java
b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java
index 2a0821c5c0a..2e8b36da446 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.CheckAndRepairVolumeCmd;
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;
@@ -379,6 +381,9 @@ public class VolumeApiServiceImpl extends ManagerBase
implements VolumeApiServic
public static ConfigKey<Long> storageTagRuleExecutionTimeout = new
ConfigKey<>("Advanced", Long.class, "storage.tag.rule.execution.timeout",
"2000", "The maximum runtime,"
+ " in milliseconds, to execute a storage tag rule; if it is
reached, a timeout will happen.", true);
+ public static final ConfigKey<Boolean> AllowCheckAndRepairVolume = new
ConfigKey<Boolean>("Advanced", Boolean.class,
"volume.check.and.repair.leaks.before.use", "false",
+ "To check and repair the volume if it has any leaks before
performing volume attach or VM start operations", true,
ConfigKey.Scope.StoragePool);
+
private final StateMachine2<Volume.State, Volume.Event, Volume>
_volStateMachine;
private static final Set<Volume.State> STATES_VOLUME_CANNOT_BE_DESTROYED =
new HashSet<>(Arrays.asList(Volume.State.Destroy, Volume.State.Expunging,
Volume.State.Expunged, Volume.State.Allocated));
@@ -1335,7 +1340,7 @@ public class VolumeApiServiceImpl extends ManagerBase
implements VolumeApiServic
outcome.get();
} catch (InterruptedException e) {
throw new RuntimeException("Operation was interrupted", e);
- } catch (java.util.concurrent.ExecutionException e) {
+ } catch (ExecutionException e) {
throw new RuntimeException("Execution exception", e);
}
@@ -1817,7 +1822,158 @@ 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(CheckAndRepairVolumeCmd
cmd) throws ResourceAllocationException {
+ long volumeId = cmd.getId();
+ String repair = cmd.getRepair();
+ final VolumeVO volume = _volsDao.findById(volumeId);
+ validationsForCheckVolumeOperation(volume);
+
+ Long vmId = volume.getInstanceId();
+ if (vmId != null) {
+ // serialize VM operation
+ return handleCheckAndRepairVolumeJob(vmId, volumeId, repair);
+ } else {
+ return handleCheckAndRepairVolume(volumeId, repair);
+ }
+ }
+
+ private Pair<String, String> handleCheckAndRepairVolume(Long volumeId,
String repair) {
+ 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> handleCheckAndRepairVolumeJob(Long vmId, Long
volumeId, String repair) throws ResourceAllocationException {
+ AsyncJobExecutionContext jobContext =
AsyncJobExecutionContext.getCurrentExecutionContext();
+ if
(jobContext.isJobDispatchedBy(VmWorkConstants.VM_WORK_JOB_DISPATCHER)) {
+ // avoid re-entrance
+ VmWorkJobVO placeHolder = null;
+ placeHolder = createPlaceHolderWork(vmId);
+ try {
+ Pair<String, String> result =
orchestrateCheckAndRepairVolume(volumeId, repair);
+ return result;
+ } finally {
+ _workJobDao.expunge(placeHolder.getId());
+ }
+ } else {
+ Outcome<Pair> outcome = checkAndRepairVolumeThroughJobQueue(vmId,
volumeId, repair);
+ try {
+ outcome.get();
+ } catch (InterruptedException e) {
+ throw new RuntimeException("Operation is interrupted", e);
+ } catch (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;
+ }
+ }
+
+ protected void validationsForCheckVolumeOperation(VolumeVO volume) {
+ Account caller = CallContext.current().getCallingAccount();
+ _accountMgr.checkAccess(caller, null, true, volume);
+
+ String volumeName = volume.getName();
+ Long vmId = volume.getInstanceId();
+ if (vmId != null) {
+ validateVMforCheckVolumeOperation(vmId, volumeName);
+ }
+
+ if (volume.getState() != Volume.State.Ready) {
+ throw new InvalidParameterValueException(String.format("Volume: %s
is not in Ready state", volumeName));
+ }
+
+ 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 (!Arrays.asList(ImageFormat.QCOW2,
ImageFormat.VDI).contains(volume.getFormat())) {
+ throw new InvalidParameterValueException("Volume format is not
supported for checking and repair");
+ }
+ }
+
+ private void validateVMforCheckVolumeOperation(Long vmId, String
volumeName) {
+ Account caller = CallContext.current().getCallingAccount();
+ UserVmVO vm = _userVmDao.findById(vmId);
+ if (vm == null) {
+ throw new InvalidParameterValueException(String.format("VM not
found, please check the VM to which this volume %s is attached", volumeName));
+ }
+
+ _accountMgr.checkAccess(caller, null, true, vm);
+
+ if (vm.getState() != State.Stopped) {
+ throw new InvalidParameterValueException(String.format("VM to
which the volume %s is attached should be in stopped state", volumeName));
+ }
+ }
+
+ private Pair<String, String> orchestrateCheckAndRepairVolume(Long
volumeId, String repair) {
+
+ VolumeInfo volume = volFactory.getVolume(volumeId);
+
+ if (volume == null) {
+ throw new InvalidParameterValueException("Checking volume and
repairing failed due to volume:" + volumeId + " doesn't exist");
+ }
+
+ CheckAndRepairVolumePayload payload = new
CheckAndRepairVolumePayload(repair);
+ volume.addPayload(payload);
+
+ return volService.checkAndRepairVolume(volume);
+ }
+
+ public Outcome<Pair> checkAndRepairVolumeThroughJobQueue(final Long vmId,
final Long volumeId, String 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")
@@ -1987,7 +2143,7 @@ public class VolumeApiServiceImpl extends ManagerBase
implements VolumeApiServic
outcome.get();
} catch (InterruptedException e) {
throw new RuntimeException("Operation was interrupted", e);
- } catch (java.util.concurrent.ExecutionException e) {
+ } catch (ExecutionException e) {
throw new RuntimeException("Execution exception", e);
}
@@ -2773,7 +2929,7 @@ public class VolumeApiServiceImpl extends ManagerBase
implements VolumeApiServic
outcome.get();
} catch (InterruptedException e) {
throw new RuntimeException("Operation is interrupted", e);
- } catch (java.util.concurrent.ExecutionException e) {
+ } catch (ExecutionException e) {
throw new RuntimeException("Execution excetion", e);
}
@@ -3181,7 +3337,7 @@ public class VolumeApiServiceImpl extends ManagerBase
implements VolumeApiServic
outcome.get();
} catch (InterruptedException e) {
throw new RuntimeException("Operation is interrupted", e);
- } catch (java.util.concurrent.ExecutionException e) {
+ } catch (ExecutionException e) {
throw new RuntimeException("Execution excetion", e);
}
@@ -3510,7 +3666,7 @@ public class VolumeApiServiceImpl extends ManagerBase
implements VolumeApiServic
outcome.get();
} catch (InterruptedException e) {
throw new RuntimeException("Operation is interrupted", e);
- } catch (java.util.concurrent.ExecutionException e) {
+ } catch (ExecutionException e) {
throw new RuntimeException("Execution excetion", e);
}
@@ -3827,7 +3983,7 @@ public class VolumeApiServiceImpl extends ManagerBase
implements VolumeApiServic
outcome.get();
} catch (InterruptedException e) {
throw new RuntimeException("Operation is interrupted", e);
- } catch (java.util.concurrent.ExecutionException e) {
+ } catch (ExecutionException e) {
throw new RuntimeException("Execution excetion", e);
}
@@ -4251,6 +4407,12 @@ public class VolumeApiServiceImpl extends ManagerBase
implements VolumeApiServic
try {
// if we don't have a host, the VM we are attaching the disk to
has never been started before
if (host != null) {
+ try {
+
volService.checkAndRepairVolumeBasedOnConfig(volFactory.getVolume(volumeToAttach.getId()),
host);
+ } catch (Exception e) {
+ s_logger.debug(String.format("Unable to check and repair
volume [%s] on host [%s], due to %s.", volumeToAttach.getName(), host,
e.getMessage()));
+ }
+
try {
volService.grantAccess(volFactory.getVolume(volumeToAttach.getId()), host,
dataStore);
} catch (Exception e) {
@@ -4596,6 +4758,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 +5013,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>
orchestrateCheckAndRepairVolume(VmWorkCheckAndRepairVolume work) throws
Exception {
+ Account account = _accountDao.findById(work.getAccountId());
+ Pair<String, String> result =
orchestrateCheckAndRepairVolume(work.getVolumeId(), work.getRepair());
+ 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);
@@ -4869,7 +5056,8 @@ public class VolumeApiServiceImpl extends ManagerBase
implements VolumeApiServic
AllowUserExpungeRecoverVolume,
MatchStoragePoolTagsWithDiskOffering,
UseHttpsToUpload,
- WaitDetachDevice
+ WaitDetachDevice,
+ AllowCheckAndRepairVolume
};
}
}
diff --git
a/server/src/test/java/com/cloud/storage/VolumeApiServiceImplTest.java
b/server/src/test/java/com/cloud/storage/VolumeApiServiceImplTest.java
index a41cbb82704..b017a2d3371 100644
--- a/server/src/test/java/com/cloud/storage/VolumeApiServiceImplTest.java
+++ b/server/src/test/java/com/cloud/storage/VolumeApiServiceImplTest.java
@@ -25,6 +25,7 @@ import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -39,6 +40,7 @@ import java.util.concurrent.ExecutionException;
import org.apache.cloudstack.acl.ControlledEntity;
import org.apache.cloudstack.acl.SecurityChecker.AccessType;
+import org.apache.cloudstack.api.command.user.volume.CheckAndRepairVolumeCmd;
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.MigrateVolumeCmd;
@@ -1645,7 +1647,6 @@ public class VolumeApiServiceImplTest {
Mockito.when(_diskOfferingDao.findById(1L)).thenReturn(diskOffering);
StoragePoolVO srcStoragePoolVOMock =
Mockito.mock(StoragePoolVO.class);
- StoragePool destPool = Mockito.mock(StoragePool.class);
PrimaryDataStore dataStore = Mockito.mock(PrimaryDataStore.class);
Mockito.when(vol.getPassphraseId()).thenReturn(1L);
@@ -1660,4 +1661,166 @@ public class VolumeApiServiceImplTest {
// test passed
}
}
+
+ @Test
+ public void testValidationsForCheckVolumeAPI() {
+ VolumeVO volume = mock(VolumeVO.class);
+
+ AccountVO account = new AccountVO("admin", 1L, "networkDomain",
Account.Type.NORMAL, "uuid");
+ UserVO user = new UserVO(1, "testuser", "password", "firstname",
"lastName", "email", "timezone", UUID.randomUUID().toString(),
User.Source.UNKNOWN);
+ CallContext.register(user, account);
+
+
lenient().doNothing().when(accountManagerMock).checkAccess(any(Account.class),
any(AccessType.class), any(Boolean.class), any(ControlledEntity.class));
+
+ when(volume.getInstanceId()).thenReturn(1L);
+ UserVmVO vm = mock(UserVmVO.class);
+ when(userVmDaoMock.findById(1L)).thenReturn(vm);
+ when(vm.getState()).thenReturn(State.Stopped);
+ when(volume.getState()).thenReturn(Volume.State.Ready);
+ when(volume.getId()).thenReturn(1L);
+
when(volumeDaoMock.getHypervisorType(1L)).thenReturn(HypervisorType.KVM);
+ when(volume.getFormat()).thenReturn(Storage.ImageFormat.QCOW2);
+
+ volumeApiServiceImpl.validationsForCheckVolumeOperation(volume);
+ }
+
+ @Test(expected = InvalidParameterValueException.class)
+ public void testValidationsForCheckVolumeAPIWithRunningVM() {
+ VolumeVO volume = mock(VolumeVO.class);
+
+ AccountVO account = new AccountVO("admin", 1L, "networkDomain",
Account.Type.NORMAL, "uuid");
+ UserVO user = new UserVO(1, "testuser", "password", "firstname",
"lastName", "email", "timezone", UUID.randomUUID().toString(),
User.Source.UNKNOWN);
+ CallContext.register(user, account);
+
+
lenient().doNothing().when(accountManagerMock).checkAccess(any(Account.class),
any(AccessType.class), any(Boolean.class), any(ControlledEntity.class));
+
+ when(volume.getInstanceId()).thenReturn(1L);
+ UserVmVO vm = mock(UserVmVO.class);
+ when(userVmDaoMock.findById(1L)).thenReturn(vm);
+ when(vm.getState()).thenReturn(State.Running);
+
+ volumeApiServiceImpl.validationsForCheckVolumeOperation(volume);
+ }
+
+ @Test(expected = InvalidParameterValueException.class)
+ public void testValidationsForCheckVolumeAPIWithNonexistedVM() {
+ VolumeVO volume = mock(VolumeVO.class);
+
+ AccountVO account = new AccountVO("admin", 1L, "networkDomain",
Account.Type.NORMAL, "uuid");
+ UserVO user = new UserVO(1, "testuser", "password", "firstname",
"lastName", "email", "timezone", UUID.randomUUID().toString(),
User.Source.UNKNOWN);
+ CallContext.register(user, account);
+
+
lenient().doNothing().when(accountManagerMock).checkAccess(any(Account.class),
any(AccessType.class), any(Boolean.class), any(ControlledEntity.class));
+
+ when(volume.getInstanceId()).thenReturn(1L);
+ when(userVmDaoMock.findById(1L)).thenReturn(null);
+
+ volumeApiServiceImpl.validationsForCheckVolumeOperation(volume);
+ }
+
+ @Test(expected = InvalidParameterValueException.class)
+ public void testValidationsForCheckVolumeAPIWithAllocatedVolume() {
+ VolumeVO volume = mock(VolumeVO.class);
+
+ AccountVO account = new AccountVO("admin", 1L, "networkDomain",
Account.Type.NORMAL, "uuid");
+ UserVO user = new UserVO(1, "testuser", "password", "firstname",
"lastName", "email", "timezone", UUID.randomUUID().toString(),
User.Source.UNKNOWN);
+ CallContext.register(user, account);
+
+
lenient().doNothing().when(accountManagerMock).checkAccess(any(Account.class),
any(AccessType.class), any(Boolean.class), any(ControlledEntity.class));
+
+ when(volume.getInstanceId()).thenReturn(1L);
+ UserVmVO vm = mock(UserVmVO.class);
+ when(userVmDaoMock.findById(1L)).thenReturn(vm);
+ when(vm.getState()).thenReturn(State.Stopped);
+ when(volume.getState()).thenReturn(Volume.State.Allocated);
+
+ volumeApiServiceImpl.validationsForCheckVolumeOperation(volume);
+ }
+
+ @Test(expected = InvalidParameterValueException.class)
+ public void testValidationsForCheckVolumeAPIWithNonKVMhypervisor() {
+ VolumeVO volume = mock(VolumeVO.class);
+
+ AccountVO account = new AccountVO("admin", 1L, "networkDomain",
Account.Type.NORMAL, "uuid");
+ UserVO user = new UserVO(1, "testuser", "password", "firstname",
"lastName", "email", "timezone", UUID.randomUUID().toString(),
User.Source.UNKNOWN);
+ CallContext.register(user, account);
+
+
lenient().doNothing().when(accountManagerMock).checkAccess(any(Account.class),
any(AccessType.class), any(Boolean.class), any(ControlledEntity.class));
+
+ when(volume.getInstanceId()).thenReturn(1L);
+ UserVmVO vm = mock(UserVmVO.class);
+ when(userVmDaoMock.findById(1L)).thenReturn(vm);
+ when(vm.getState()).thenReturn(State.Stopped);
+ when(volume.getState()).thenReturn(Volume.State.Ready);
+ when(volume.getId()).thenReturn(1L);
+
when(volumeDaoMock.getHypervisorType(1L)).thenReturn(HypervisorType.VMware);
+
+ volumeApiServiceImpl.validationsForCheckVolumeOperation(volume);
+ }
+
+ @Test
+ public void testCheckAndRepairVolume() throws ResourceAllocationException {
+
+ CheckAndRepairVolumeCmd cmd = mock(CheckAndRepairVolumeCmd.class);
+ when(cmd.getId()).thenReturn(1L);
+ when(cmd.getRepair()).thenReturn(null);
+
+ VolumeVO volume = mock(VolumeVO.class);
+ when(volumeDaoMock.findById(1L)).thenReturn(volume);
+
+ AccountVO account = new AccountVO("admin", 1L, "networkDomain",
Account.Type.NORMAL, "uuid");
+ UserVO user = new UserVO(1, "testuser", "password", "firstname",
"lastName", "email", "timezone", UUID.randomUUID().toString(),
User.Source.UNKNOWN);
+ CallContext.register(user, account);
+
+
lenient().doNothing().when(accountManagerMock).checkAccess(any(Account.class),
any(AccessType.class), any(Boolean.class), any(ControlledEntity.class));
+
+ when(volume.getInstanceId()).thenReturn(null);
+ when(volume.getState()).thenReturn(Volume.State.Ready);
+ when(volume.getId()).thenReturn(1L);
+
when(volumeDaoMock.getHypervisorType(1L)).thenReturn(HypervisorType.KVM);
+
+ VolumeInfo volumeInfo = mock(VolumeInfo.class);
+ when(volumeDataFactoryMock.getVolume(1L)).thenReturn(volumeInfo);
+
+ String checkResult = "{\n" +
+ " \"image-end-offset\": 6442582016,\n" +
+ " \"total-clusters\": 163840,\n" +
+ " \"check-errors\": 0,\n" +
+ " \"leaks\": 124,\n" +
+ " \"allocated-clusters\": 98154,\n" +
+ " \"filename\":
\"/var/lib/libvirt/images/26be20c7-b9d0-43f6-a76e-16c70737a0e0\",\n" +
+ " \"format\": \"qcow2\",\n" +
+ " \"fragmented-clusters\": 96135\n" +
+ "}";
+
+ String repairResult = null;
+ Pair<String, String> result = new Pair<>(checkResult, repairResult);
+
when(volumeServiceMock.checkAndRepairVolume(volumeInfo)).thenReturn(result);
+ when(volume.getFormat()).thenReturn(Storage.ImageFormat.QCOW2);
+
+ Pair<String, String> finalresult =
volumeApiServiceImpl.checkAndRepairVolume(cmd);
+
+ Assert.assertEquals(result, finalresult);
+ }
+
+ @Test(expected = InvalidParameterValueException.class)
+ public void testValidationsForCheckVolumeAPIWithInvalidVolumeFormat() {
+ VolumeVO volume = mock(VolumeVO.class);
+ AccountVO account = new AccountVO("admin", 1L, "networkDomain",
Account.Type.NORMAL, "uuid");
+ UserVO user = new UserVO(1, "testuser", "password", "firstname",
"lastName", "email", "timezone", UUID.randomUUID().toString(),
User.Source.UNKNOWN);
+ CallContext.register(user, account);
+
+
lenient().doNothing().when(accountManagerMock).checkAccess(any(Account.class),
any(AccessType.class), any(Boolean.class), any(ControlledEntity.class));
+
+ when(volume.getInstanceId()).thenReturn(1L);
+ UserVmVO vm = mock(UserVmVO.class);
+ when(userVmDaoMock.findById(1L)).thenReturn(vm);
+ when(vm.getState()).thenReturn(State.Stopped);
+ when(volume.getState()).thenReturn(Volume.State.Ready);
+ when(volume.getId()).thenReturn(1L);
+
when(volumeDaoMock.getHypervisorType(1L)).thenReturn(HypervisorType.KVM);
+ when(volume.getFormat()).thenReturn(Storage.ImageFormat.RAW);
+
+ volumeApiServiceImpl.validationsForCheckVolumeOperation(volume);
+ }
}
diff --git a/utils/src/main/java/com/cloud/utils/StringUtils.java
b/utils/src/main/java/com/cloud/utils/StringUtils.java
index 9e197a8a94b..fd4f7aba698 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.isNotBlank(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;
+ }
}
diff --git a/utils/src/main/java/com/cloud/utils/script/Script.java
b/utils/src/main/java/com/cloud/utils/script/Script.java
index 6af08a981d1..cdab31f1865 100644
--- a/utils/src/main/java/com/cloud/utils/script/Script.java
+++ b/utils/src/main/java/com/cloud/utils/script/Script.java
@@ -330,6 +330,118 @@ public class Script implements Callable<String> {
}
}
+ public String executeIgnoreExitValue(OutputInterpreter interpreter, int
exitValue) {
+ String[] command = _command.toArray(new String[_command.size()]);
+
+ if (_logger.isDebugEnabled()) {
+ _logger.debug(String.format("Executing: %s",
buildCommandLine(command).split(KeyStoreUtils.KS_FILENAME)[0]));
+ }
+
+ try {
+ ProcessBuilder pb = new ProcessBuilder(command);
+ pb.redirectErrorStream(true);
+ if (_workDir != null)
+ pb.directory(new File(_workDir));
+
+ _process = pb.start();
+ if (_process == null) {
+ _logger.warn(String.format("Unable to execute: %s",
buildCommandLine(command)));
+ return String.format("Unable to execute the command: %s",
command[0]);
+ }
+
+ BufferedReader ir = new BufferedReader(new
InputStreamReader(_process.getInputStream()));
+
+ _thread = Thread.currentThread();
+ ScheduledFuture<String> future = null;
+ if (_timeout > 0) {
+ future = s_executors.schedule(this, _timeout,
TimeUnit.MILLISECONDS);
+ }
+
+ Task task = null;
+ if (interpreter != null && interpreter.drain()) {
+ task = new Task(interpreter, ir);
+ s_executors.execute(task);
+ }
+
+ while (true) {
+ _logger.debug(String.format("Executing while with timeout :
%d", _timeout));
+ try {
+ //process execution completed within timeout period
+ if (_process.waitFor(_timeout, TimeUnit.MILLISECONDS)) {
+ //process completed successfully
+ if (_process.exitValue() == 0 || _process.exitValue()
== exitValue) {
+ _logger.debug("Execution is successful.");
+ if (interpreter != null) {
+ return interpreter.drain() ? task.getResult()
: interpreter.interpret(ir);
+ } else {
+ // null return exitValue apparently
+ return String.valueOf(_process.exitValue());
+ }
+ } else { //process failed
+ break;
+ }
+ } //timeout
+ } catch (InterruptedException e) {
+ if (!_isTimeOut) {
+ /*
+ * This is not timeout, we are interrupted by others,
+ * continue
+ */
+ _logger.debug("We are interrupted but it's not a
timeout, just continue");
+ continue;
+ }
+ } finally {
+ if (future != null) {
+ future.cancel(false);
+ }
+ Thread.interrupted();
+ }
+
+ //timeout without completing the process
+ TimedOutLogger log = new TimedOutLogger(_process);
+ Task timedoutTask = new Task(log, ir);
+
+ timedoutTask.run();
+ if (!_passwordCommand) {
+ _logger.warn(String.format("Timed out: %s. Output is:
%s", buildCommandLine(command), timedoutTask.getResult()));
+ } else {
+ _logger.warn(String.format("Timed out: %s",
buildCommandLine(command)));
+ }
+
+ return ERR_TIMEOUT;
+ }
+
+ _logger.debug(String.format("Exit value is %d",
_process.exitValue()));
+
+ BufferedReader reader = new BufferedReader(new
InputStreamReader(_process.getInputStream()), 128);
+
+ String error;
+ if (interpreter != null) {
+ error = interpreter.processError(reader);
+ } else {
+ error = String.valueOf(_process.exitValue());
+ }
+
+ if (_logger.isDebugEnabled()) {
+ _logger.debug(error);
+ }
+ return error;
+ } catch (SecurityException ex) {
+ _logger.warn("Security Exception....not running as root?", ex);
+ return stackTraceAsString(ex);
+ } catch (Exception ex) {
+ _logger.warn(String.format("Exception: %s",
buildCommandLine(command)), ex);
+ return stackTraceAsString(ex);
+ } finally {
+ if (_process != null) {
+ IOUtils.closeQuietly(_process.getErrorStream());
+ IOUtils.closeQuietly(_process.getOutputStream());
+ IOUtils.closeQuietly(_process.getInputStream());
+ _process.destroyForcibly();
+ }
+ }
+ }
+
@Override
public String call() {
try {
@@ -563,4 +675,24 @@ public class Script implements Callable<String> {
}
}
+ public static String runBashScriptIgnoreExitValue(String command, int
exitValue) {
+ return runBashScriptIgnoreExitValue(command, exitValue, 0);
+ }
+
+ public static String runBashScriptIgnoreExitValue(String command, int
exitValue, int timeout) {
+
+ Script s = new Script("/bin/bash", timeout);
+ s.add("-c");
+ s.add(command);
+
+ OutputInterpreter.AllLinesParser parser = new
OutputInterpreter.AllLinesParser();
+ s.executeIgnoreExitValue(parser, exitValue);
+
+ String result = parser.getLines();
+ if (result == null || result.trim().isEmpty()) {
+ return null;
+ } else {
+ return result.trim();
+ }
+ }
}