host = volumeManager.findConnectedKvmHostByPrimaryStorage(
+ primaryStorageUuid, "external primary storage", operation);
+ if (!host.isSuccess()) {
+ throw new OperationFailureException(host.error);
+ }
+ return host.result;
+ }
+}
diff --git a/storage/src/main/java/org/zstack/storage/volume/VolumeInPlaceEncryptor.java b/storage/src/main/java/org/zstack/storage/volume/VolumeInPlaceEncryptor.java
index 1a809091cc5..8ed0e62f9c9 100644
--- a/storage/src/main/java/org/zstack/storage/volume/VolumeInPlaceEncryptor.java
+++ b/storage/src/main/java/org/zstack/storage/volume/VolumeInPlaceEncryptor.java
@@ -13,18 +13,25 @@
import org.zstack.header.errorcode.OperationFailureException;
import org.zstack.header.keyprovider.EncryptedResourceKeyManager;
import org.zstack.header.message.MessageReply;
+import org.zstack.header.storage.primary.DeleteVolumeBitsOnPrimaryStorageMsg;
import org.zstack.header.storage.primary.EncryptVolumeBitsOnPrimaryStorageMsg;
+import org.zstack.header.storage.primary.EncryptVolumeBitsOnPrimaryStorageReply;
import org.zstack.header.storage.primary.PrimaryStorageConstant;
+import org.zstack.header.volume.VolumeFormat;
import org.zstack.header.volume.VolumeVO;
import org.zstack.storage.encrypt.VolumeEncryptedResourceKeyBackend;
import org.zstack.storage.encrypt.VolumeEncryptedSecretHelper;
+import org.zstack.storage.primary.PrimaryStorageDeleteBitGC;
+import org.zstack.storage.primary.PrimaryStorageGlobalConfig;
import org.zstack.utils.Utils;
import org.zstack.utils.logging.CLogger;
+import java.util.concurrent.TimeUnit;
+
import static org.zstack.core.Platform.operr;
/**
- * Performs an in-place LUKS conversion of an existing volume's bits.
+ * Performs a LUKS conversion of an existing volume's bits.
*
* This is the single source of truth for the "encrypt-in-place" workflow that was
* previously duplicated between {@link VolumeBase#handleMessage} (for the
@@ -38,9 +45,9 @@
*
Materialize a DEK via {@link EncryptedResourceKeyManager#getOrCreateKey}.
* Seal the DEK for the target host and pass it as {@code encryptedDek}
* to the primary storage backend.
- * Ask the primary storage backend to LUKS-convert the bits in place via
- * {@code EncryptVolumeBitsOnPrimaryStorageMsg}. The kvmagent creates any
- * single-use secret material file locally.
+ * Ask the primary storage backend to LUKS-convert the bits via
+ * {@code EncryptVolumeBitsOnPrimaryStorageMsg}. The backend may return a
+ * replacement install path.
* Persist {@code VolumeVO.encrypted = true} after a successful conversion.
*
*
@@ -246,7 +253,7 @@ public void fail(ErrorCode err) {
return;
}
- // 4) Ask the PS backend to LUKS-convert the bits in place.
+ // 4) Ask the PS backend to LUKS-convert the bits.
EncryptVolumeBitsOnPrimaryStorageMsg emsg = new EncryptVolumeBitsOnPrimaryStorageMsg();
emsg.setPrimaryStorageUuid(psUuid);
emsg.setHostUuid(ctx.getHostUuid());
@@ -262,17 +269,61 @@ public void run(MessageReply r) {
r.getError(), completion);
return;
}
+ EncryptVolumeBitsOnPrimaryStorageReply reply = r.castReply();
+ String newInstallPath = reply.getInstallPath();
+ boolean installPathChanged = StringUtils.isNotBlank(newInstallPath)
+ && !StringUtils.equals(newInstallPath, installPath);
+ if (installPathChanged) {
+ volume.setInstallPath(newInstallPath);
+ }
// 5) Persist encrypted=true. The short-circuit above guarantees we only
// reach here when the row was previously encrypted=false; this is the
// one and only place the flag flips, ensuring it always reflects the
// on-disk reality.
volume.setEncrypted(true);
VolumeVO latest = dbf.updateAndRefresh(volume);
+ if (installPathChanged) {
+ deleteOldVolumeBits(psUuid, latest, installPath, completion);
+ return;
+ }
completion.success(latest);
}
});
}
+ void deleteOldVolumeBits(String psUuid, VolumeVO volume, String oldInstallPath,
+ ReturnValueCompletion completion) {
+ DeleteVolumeBitsOnPrimaryStorageMsg dmsg = new DeleteVolumeBitsOnPrimaryStorageMsg();
+ dmsg.setPrimaryStorageUuid(psUuid);
+ dmsg.setInstallPath(oldInstallPath);
+ dmsg.setBitsUuid(volume.getUuid());
+ dmsg.setBitsType(VolumeVO.class.getSimpleName());
+ dmsg.setHypervisorType(VolumeFormat.getMasterHypervisorTypeByVolumeFormat(volume.getFormat()).toString());
+ dmsg.setSize(volume.getSize());
+ bus.makeTargetServiceIdByResourceUuid(dmsg, PrimaryStorageConstant.SERVICE_ID, psUuid);
+ bus.send(dmsg, new CloudBusCallBack(completion) {
+ @Override
+ public void run(MessageReply reply) {
+ if (reply.isSuccess()) {
+ completion.success(volume);
+ return;
+ }
+
+ PrimaryStorageDeleteBitGC gc = new PrimaryStorageDeleteBitGC();
+ gc.NAME = String.format("gc-delete-old-bits-volume-%s-on-primary-storage-%s", volume.getUuid(), psUuid);
+ gc.primaryStorageInstallPath = oldInstallPath;
+ gc.primaryStorageUuid = psUuid;
+ gc.volume = volume;
+ gc.submit(PrimaryStorageGlobalConfig.PRIMARY_STORAGE_DELETEBITS_GARBAGE_COLLECTOR_INTERVAL.value(Long.class),
+ TimeUnit.SECONDS);
+ logger.warn(String.format(
+ "failed to delete old volume bits[installPath:%s] after encrypting volume[uuid:%s] in place, cleanup GC has been submitted: %s",
+ oldInstallPath, volume.getUuid(), reply.getError()));
+ completion.success(volume);
+ }
+ });
+ }
+
private void failAfterRollingBackMetadata(String volumeUuid,
boolean attachedProviderHere,
EncryptedResourceKeyManager.ResourceKeyResult keyResult,
diff --git a/storage/src/main/java/org/zstack/storage/volume/VolumeManagerImpl.java b/storage/src/main/java/org/zstack/storage/volume/VolumeManagerImpl.java
index bf032d5df69..246279580db 100755
--- a/storage/src/main/java/org/zstack/storage/volume/VolumeManagerImpl.java
+++ b/storage/src/main/java/org/zstack/storage/volume/VolumeManagerImpl.java
@@ -514,6 +514,9 @@ public void run(FlowTrigger trigger, Map data) {
volumeInPlaceEncryptor.encryptInPlace(vol, ctx, new ReturnValueCompletion(trigger) {
@Override
public void success(VolumeVO latest) {
+ if (StringUtils.isNotBlank(latest.getInstallPath())) {
+ primaryStorageInstallPath = latest.getInstallPath();
+ }
trigger.next();
}
diff --git a/test/src/test/groovy/org/zstack/storage/volume/VolumeInPlaceEncryptorTest.groovy b/test/src/test/groovy/org/zstack/storage/volume/VolumeInPlaceEncryptorTest.groovy
new file mode 100644
index 00000000000..f87a0039a72
--- /dev/null
+++ b/test/src/test/groovy/org/zstack/storage/volume/VolumeInPlaceEncryptorTest.groovy
@@ -0,0 +1,108 @@
+package org.zstack.storage.volume
+
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito
+import org.zstack.core.cloudbus.CloudBus
+import org.zstack.core.cloudbus.CloudBusCallBack
+import org.zstack.header.core.ReturnValueCompletion
+import org.zstack.header.errorcode.ErrorCode
+import org.zstack.header.host.HypervisorType
+import org.zstack.header.message.MessageReply
+import org.zstack.header.storage.primary.DeleteVolumeBitsOnPrimaryStorageMsg
+import org.zstack.header.vm.VmInstanceConstant
+import org.zstack.header.volume.VolumeConstant
+import org.zstack.header.volume.VolumeFormat
+import org.zstack.header.volume.VolumeVO
+
+import java.lang.reflect.Field
+
+class VolumeInPlaceEncryptorTest {
+ private VolumeInPlaceEncryptor encryptor
+ private CloudBus bus
+ private DeleteVolumeBitsOnPrimaryStorageMsg sentMsg
+ private CloudBusCallBack sentCallback
+
+ @Before
+ void setUp() {
+ if (!HypervisorType.hasType(VmInstanceConstant.KVM_HYPERVISOR_TYPE)) {
+ new HypervisorType(VmInstanceConstant.KVM_HYPERVISOR_TYPE)
+ }
+ if (!VolumeFormat.hasType(VolumeConstant.VOLUME_FORMAT_RAW)) {
+ new VolumeFormat(VolumeConstant.VOLUME_FORMAT_RAW,
+ HypervisorType.valueOf(VmInstanceConstant.KVM_HYPERVISOR_TYPE))
+ }
+
+ encryptor = new VolumeInPlaceEncryptor()
+ sentMsg = null
+ sentCallback = null
+ bus = Mockito.mock(CloudBus.class, { invocation ->
+ if (invocation.method.name == "send"
+ && invocation.arguments.length == 2
+ && invocation.arguments[0] instanceof DeleteVolumeBitsOnPrimaryStorageMsg
+ && invocation.arguments[1] instanceof CloudBusCallBack) {
+ sentMsg = invocation.arguments[0] as DeleteVolumeBitsOnPrimaryStorageMsg
+ sentCallback = invocation.arguments[1] as CloudBusCallBack
+ }
+ return null
+ } as org.mockito.stubbing.Answer)
+ setField("bus", bus)
+ }
+
+ @Test
+ void testDeleteOldVolumeBitsWaitsForDeleteSuccess() {
+ VolumeVO volume = volume()
+ Map result = invokeDeleteOldVolumeBits(volume)
+
+ MessageReply reply = new MessageReply()
+ result.callback.run(reply)
+
+ assert result.success
+ assert result.volume == volume
+ assert result.error == null
+ }
+
+ private Map invokeDeleteOldVolumeBits(VolumeVO volume) {
+ Map result = [success: false, volume: null, error: null, msg: null, callback: null]
+
+ encryptor.deleteOldVolumeBits("ps-uuid", volume, "old-install-path",
+ new ReturnValueCompletion(null) {
+ @Override
+ void success(VolumeVO returnValue) {
+ result.success = true
+ result.volume = returnValue
+ }
+
+ @Override
+ void fail(ErrorCode errorCode) {
+ result.error = errorCode
+ }
+ })
+
+ DeleteVolumeBitsOnPrimaryStorageMsg msg = sentMsg
+ assert msg != null
+ assert msg.primaryStorageUuid == "ps-uuid"
+ assert msg.installPath == "old-install-path"
+ assert msg.bitsUuid == volume.uuid
+ assert msg.bitsType == VolumeVO.class.simpleName
+ assert msg.size == volume.size
+
+ result.callback = sentCallback
+ assert sentCallback != null
+ return result
+ }
+
+ private VolumeVO volume() {
+ VolumeVO volume = new VolumeVO()
+ volume.uuid = "volume-uuid"
+ volume.format = VolumeConstant.VOLUME_FORMAT_RAW
+ volume.size = 1024L
+ return volume
+ }
+
+ private void setField(String name, Object value) {
+ Field field = VolumeInPlaceEncryptor.class.getDeclaredField(name)
+ field.accessible = true
+ field.set(encryptor, value)
+ }
+}
diff --git a/test/src/test/groovy/org/zstack/storage/volume/VolumeManagerImplHostSelectionTest.groovy b/test/src/test/groovy/org/zstack/storage/volume/VolumeManagerImplHostSelectionTest.groovy
index a6e1505cc81..071f7d09a12 100644
--- a/test/src/test/groovy/org/zstack/storage/volume/VolumeManagerImplHostSelectionTest.groovy
+++ b/test/src/test/groovy/org/zstack/storage/volume/VolumeManagerImplHostSelectionTest.groovy
@@ -76,6 +76,21 @@ class VolumeManagerImplHostSelectionTest {
Mockito.verify(hostQuery).add(HostVO_.uuid, SimpleQuery.Op.IN, Arrays.asList("host-connected"))
}
+ @Test
+ void testDisconnectedPsHostRefsDoNotFallback() {
+ Mockito.when(clusterRefQuery.listValue()).thenReturn(Arrays.asList("cluster-uuid"))
+ Mockito.when(hostRefQuery.list()).thenReturn(Arrays.asList(
+ primaryStorageHostRef("host-disconnected-1", PrimaryStorageHostStatus.Disconnected),
+ primaryStorageHostRef("host-disconnected-2", PrimaryStorageHostStatus.Disconnected)
+ ))
+
+ HostInventory inventory = invokeSelectHostForEncryptInPlace()
+
+ assert inventory == null : "encrypt-in-place should not fallback to arbitrary cluster host when PS has only disconnected host refs"
+ Mockito.verify(hostQuery, Mockito.never()).add(Mockito.eq(HostVO_.uuid), Mockito.eq(SimpleQuery.Op.IN), Mockito.anyList())
+ Mockito.verify(hostQuery, Mockito.never()).find()
+ }
+
private HostInventory invokeSelectHostForEncryptInPlace() {
Method method = VolumeManagerImpl.class.getDeclaredMethod("selectHostForEncryptInPlace", String.class)
method.accessible = true
diff --git a/test/src/test/groovy/org/zstack/test/unittest/storage/encrypt/ZbsVolumeEncryptionExtensionTest.groovy b/test/src/test/groovy/org/zstack/test/unittest/storage/encrypt/ZbsVolumeEncryptionExtensionTest.groovy
new file mode 100644
index 00000000000..7c997fc8d14
--- /dev/null
+++ b/test/src/test/groovy/org/zstack/test/unittest/storage/encrypt/ZbsVolumeEncryptionExtensionTest.groovy
@@ -0,0 +1,61 @@
+package org.zstack.test.unittest.storage.encrypt
+
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito
+import org.zstack.header.storage.snapshot.VolumeSnapshotInventory
+import org.zstack.header.volume.VolumeVO
+import org.zstack.storage.encrypt.VolumeSnapshotEncryptionHelper
+import org.zstack.storage.encrypt.ZbsVolumeEncryptionExtension
+
+import java.lang.reflect.Field
+
+class ZbsVolumeEncryptionExtensionTest {
+ private ZbsVolumeEncryptionExtension extension
+ private VolumeSnapshotEncryptionHelper snapshotEncryptionHelper
+
+ @Before
+ void setUp() {
+ extension = new ZbsVolumeEncryptionExtension()
+ snapshotEncryptionHelper = Mockito.mock(VolumeSnapshotEncryptionHelper.class)
+ setField("snapshotEncryptionHelper", snapshotEncryptionHelper)
+ }
+
+ @Test
+ void testSnapshotHooksDelegateForEncryptedVolume() {
+ VolumeVO volume = new VolumeVO()
+ volume.uuid = "volume-uuid"
+ volume.encrypted = true
+
+ VolumeSnapshotInventory snapshot = new VolumeSnapshotInventory()
+ snapshot.uuid = "snapshot-uuid"
+
+ extension.beforeTakeSnapshot("ps-uuid", volume, snapshot)
+ extension.afterTakeSnapshot("ps-uuid", volume, snapshot)
+
+ Mockito.verify(snapshotEncryptionHelper).inheritVolumeKeyToSnapshot(volume, snapshot)
+ Mockito.verify(snapshotEncryptionHelper).completeTakeSnapshot(volume, snapshot)
+ }
+
+ @Test
+ void testSnapshotHooksSkipPlainVolume() {
+ VolumeVO volume = new VolumeVO()
+ volume.uuid = "volume-uuid"
+ volume.encrypted = false
+
+ VolumeSnapshotInventory snapshot = new VolumeSnapshotInventory()
+ snapshot.uuid = "snapshot-uuid"
+
+ extension.beforeTakeSnapshot("ps-uuid", volume, snapshot)
+ extension.afterTakeSnapshot("ps-uuid", volume, snapshot)
+
+ Mockito.verify(snapshotEncryptionHelper, Mockito.never()).inheritVolumeKeyToSnapshot(volume, snapshot)
+ Mockito.verify(snapshotEncryptionHelper, Mockito.never()).completeTakeSnapshot(volume, snapshot)
+ }
+
+ private void setField(String name, Object value) {
+ Field field = ZbsVolumeEncryptionExtension.class.getDeclaredField(name)
+ field.accessible = true
+ field.set(extension, value)
+ }
+}