Skip to content

Commit ed95144

Browse files
committed
Linstor: encryption support (apache#10126)
This introduces a new encryption mode, instead of a simple bool. Now also storage driver can just provide encrypted volumes to CloudStack.
1 parent e713c2d commit ed95144

7 files changed

Lines changed: 344 additions & 30 deletions

File tree

api/src/main/java/com/cloud/storage/Storage.java

Lines changed: 46 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -129,34 +129,51 @@ public static enum TemplateType {
129129
ISODISK /* Template corresponding to a iso (non root disk) present in an OVA */
130130
}
131131

132+
public enum EncryptionSupport {
133+
/**
134+
* Encryption not supported.
135+
*/
136+
Unsupported,
137+
/**
138+
* Will use hypervisor encryption driver (qemu -> luks)
139+
*/
140+
Hypervisor,
141+
/**
142+
* Storage pool handles encryption and just provides an encrypted volume
143+
*/
144+
Storage
145+
}
146+
132147
public static enum StoragePoolType {
133-
Filesystem(false, true), // local directory
134-
NetworkFilesystem(true, true), // NFS
135-
IscsiLUN(true, false), // shared LUN, with a clusterfs overlay
136-
Iscsi(true, false), // for e.g., ZFS Comstar
137-
ISO(false, false), // for iso image
138-
LVM(false, false), // XenServer local LVM SR
139-
CLVM(true, false),
140-
RBD(true, true), // http://libvirt.org/storage.html#StorageBackendRBD
141-
SharedMountPoint(true, false),
142-
VMFS(true, true), // VMware VMFS storage
143-
PreSetup(true, true), // for XenServer, Storage Pool is set up by customers.
144-
EXT(false, true), // XenServer local EXT SR
145-
OCFS2(true, false),
146-
SMB(true, false),
147-
Gluster(true, false),
148-
PowerFlex(true, true), // Dell EMC PowerFlex/ScaleIO (formerly VxFlexOS)
149-
ManagedNFS(true, false),
150-
Linstor(true, true),
151-
DatastoreCluster(true, true), // for VMware, to abstract pool of clusters
152-
StorPool(true, true);
148+
Filesystem(false, true, EncryptionSupport.Hypervisor), // local directory
149+
NetworkFilesystem(true, true, EncryptionSupport.Hypervisor), // NFS
150+
IscsiLUN(true, false, EncryptionSupport.Unsupported), // shared LUN, with a clusterfs overlay
151+
Iscsi(true, false, EncryptionSupport.Unsupported), // for e.g., ZFS Comstar
152+
ISO(false, false, EncryptionSupport.Unsupported), // for iso image
153+
LVM(false, false, EncryptionSupport.Unsupported), // XenServer local LVM SR
154+
CLVM(true, false, EncryptionSupport.Unsupported),
155+
RBD(true, true, EncryptionSupport.Unsupported), // http://libvirt.org/storage.html#StorageBackendRBD
156+
SharedMountPoint(true, false, EncryptionSupport.Hypervisor),
157+
VMFS(true, true, EncryptionSupport.Unsupported), // VMware VMFS storage
158+
PreSetup(true, true, EncryptionSupport.Unsupported), // for XenServer, Storage Pool is set up by customers.
159+
EXT(false, true, EncryptionSupport.Unsupported), // XenServer local EXT SR
160+
OCFS2(true, false, EncryptionSupport.Unsupported),
161+
SMB(true, false, EncryptionSupport.Unsupported),
162+
Gluster(true, false, EncryptionSupport.Unsupported),
163+
PowerFlex(true, true, EncryptionSupport.Hypervisor), // Dell EMC PowerFlex/ScaleIO (formerly VxFlexOS)
164+
ManagedNFS(true, false, EncryptionSupport.Unsupported),
165+
Linstor(true, true, EncryptionSupport.Storage),
166+
DatastoreCluster(true, true, EncryptionSupport.Unsupported), // for VMware, to abstract pool of clusters
167+
StorPool(true, true, EncryptionSupport.Unsupported);
153168

154169
private final boolean shared;
155170
private final boolean overprovisioning;
171+
private final EncryptionSupport encryption;
156172

157-
StoragePoolType(boolean shared, boolean overprovisioning) {
173+
StoragePoolType(boolean shared, boolean overprovisioning, EncryptionSupport encryption) {
158174
this.shared = shared;
159175
this.overprovisioning = overprovisioning;
176+
this.encryption = encryption;
160177
}
161178

162179
public boolean isShared() {
@@ -166,6 +183,14 @@ public boolean isShared() {
166183
public boolean supportsOverProvisioning() {
167184
return overprovisioning;
168185
}
186+
187+
public boolean supportsEncryption() {
188+
return encryption == EncryptionSupport.Hypervisor || encryption == EncryptionSupport.Storage;
189+
}
190+
191+
public EncryptionSupport encryptionSupportMode() {
192+
return encryption;
193+
}
169194
}
170195

171196
public static List<StoragePoolType> getNonSharedStoragePoolTypes() {

plugins/storage/volume/linstor/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
- Volume snapshots on zfs used the wrong dataset path to hide/unhide snapdev
1313

14+
## [2024-12-19]
15+
16+
### Added
17+
- Native CloudStack encryption support
18+
1419
## [2024-12-13]
1520

1621
### Fixed

plugins/storage/volume/linstor/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LinstorBackupSnapshotCommandWrapper.java

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -96,18 +96,23 @@ private String convertImageToQCow2(
9696
// NOTE: the qemu img will also contain the drbd metadata at the end
9797
final QemuImg qemu = new QemuImg(waitMilliSeconds);
9898
qemu.convert(srcFile, dstFile);
99-
s_logger.info("Backup snapshot " + srcFile + " to " + dstPath);
99+
s_logger.info(String.format("Backup snapshot '%s' to '%s'", srcPath, dstPath));
100100
return dstPath;
101101
}
102102

103103
private SnapshotObjectTO setCorrectSnapshotSize(final SnapshotObjectTO dst, final String dstPath) {
104104
final File snapFile = new File(dstPath);
105-
final long size = snapFile.exists() ? snapFile.length() : 0;
105+
long size;
106+
if (snapFile.exists()) {
107+
size = snapFile.length();
108+
} else {
109+
s_logger.warn(String.format("Snapshot file %s does not exist. Reporting size 0", dstPath));
110+
size = 0;
111+
}
106112

107-
final SnapshotObjectTO snapshot = new SnapshotObjectTO();
108-
snapshot.setPath(dst.getPath() + File.separator + dst.getName());
109-
snapshot.setPhysicalSize(size);
110-
return snapshot;
113+
dst.setPath(dst.getPath() + File.separator + dst.getName());
114+
dst.setPhysicalSize(size);
115+
return dst;
111116
}
112117

113118
@Override
@@ -157,6 +162,7 @@ public CopyCmdAnswer execute(LinstorBackupSnapshotCommand cmd, LibvirtComputingR
157162
s_logger.info("Backup shrunk " + dstPath + " to actual size " + src.getVolume().getSize());
158163

159164
SnapshotObjectTO snapshot = setCorrectSnapshotSize(dst, dstPath);
165+
s_logger.info(String.format("Actual file size for '%s' is %d", dstPath, snapshot.getPhysicalSize()));
160166
return new CopyCmdAnswer(snapshot);
161167
} catch (final Exception e) {
162168
final String error = String.format("Failed to backup snapshot with id [%s] with a pool %s, due to %s",

plugins/storage/volume/linstor/src/main/java/com/cloud/hypervisor/kvm/storage/LinstorStorageAdaptor.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -407,7 +407,7 @@ private boolean tryDisconnectLinstor(String volumePath, KVMStoragePool pool)
407407
if (rsc.getFlags() != null &&
408408
rsc.getFlags().contains(ApiConsts.FLAG_DRBD_DISKLESS) &&
409409
!rsc.getFlags().contains(ApiConsts.FLAG_TIE_BREAKER)) {
410-
ApiCallRcList delAnswers = api.resourceDelete(rsc.getName(), localNodeName);
410+
ApiCallRcList delAnswers = api.resourceDelete(rsc.getName(), localNodeName, true);
411411
logLinstorAnswers(delAnswers);
412412
}
413413
} catch (ApiException apiEx) {

plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/driver/LinstorPrimaryDataStoreDriverImpl.java

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,14 @@
2121
import com.linbit.linstor.api.DevelopersApi;
2222
import com.linbit.linstor.api.model.ApiCallRc;
2323
import com.linbit.linstor.api.model.ApiCallRcList;
24+
import com.linbit.linstor.api.model.AutoSelectFilter;
25+
import com.linbit.linstor.api.model.LayerType;
2426
import com.linbit.linstor.api.model.Properties;
2527
import com.linbit.linstor.api.model.ResourceDefinition;
2628
import com.linbit.linstor.api.model.ResourceDefinitionCloneRequest;
2729
import com.linbit.linstor.api.model.ResourceDefinitionCloneStarted;
2830
import com.linbit.linstor.api.model.ResourceDefinitionCreate;
31+
import com.linbit.linstor.api.model.ResourceGroup;
2932
import com.linbit.linstor.api.model.ResourceGroupSpawn;
3033
import com.linbit.linstor.api.model.ResourceMakeAvailable;
3134
import com.linbit.linstor.api.model.Snapshot;
@@ -34,6 +37,7 @@
3437
import com.linbit.linstor.api.model.VolumeDefinitionModify;
3538

3639
import javax.annotation.Nonnull;
40+
import javax.annotation.Nullable;
3741
import javax.inject.Inject;
3842

3943
import java.util.Arrays;
@@ -42,6 +46,7 @@
4246
import java.util.Map;
4347
import java.util.Objects;
4448
import java.util.Optional;
49+
import java.util.stream.Collectors;
4550

4651
import com.cloud.agent.api.Answer;
4752
import com.cloud.agent.api.storage.ResizeVolumeAnswer;
@@ -101,8 +106,11 @@
101106
import org.apache.cloudstack.storage.to.SnapshotObjectTO;
102107
import org.apache.cloudstack.storage.to.VolumeObjectTO;
103108
import org.apache.cloudstack.storage.volume.VolumeObject;
109+
import org.apache.commons.collections.CollectionUtils;
104110
import org.apache.log4j.Logger;
105111

112+
import java.nio.charset.StandardCharsets;
113+
106114
public class LinstorPrimaryDataStoreDriverImpl implements PrimaryDataStoreDriver {
107115
private static final Logger s_logger = Logger.getLogger(LinstorPrimaryDataStoreDriverImpl.class);
108116
@Inject private PrimaryDataStoreDao _storagePoolDao;
@@ -391,11 +399,56 @@ private String getRscGrp(StoragePoolVO storagePoolVO) {
391399
storagePoolVO.getUserInfo() : "DfltRscGrp";
392400
}
393401

402+
/**
403+
* Returns the layerlist of the resourceGroup with encryption(LUKS) added above STORAGE.
404+
* If the resourceGroup layer list already contains LUKS this layer list will be returned.
405+
* @param api Linstor developers API
406+
* @param resourceGroup Resource group to get the encryption layer list
407+
* @return layer list with LUKS added
408+
*/
409+
public List<LayerType> getEncryptedLayerList(DevelopersApi api, String resourceGroup) {
410+
try {
411+
List<ResourceGroup> rscGrps = api.resourceGroupList(
412+
Collections.singletonList(resourceGroup), Collections.emptyList(), null, null);
413+
414+
if (CollectionUtils.isEmpty(rscGrps)) {
415+
throw new CloudRuntimeException(
416+
String.format("Resource Group %s not found on Linstor cluster.", resourceGroup));
417+
}
418+
419+
final ResourceGroup rscGrp = rscGrps.get(0);
420+
List<LayerType> layers = Arrays.asList(LayerType.DRBD, LayerType.LUKS, LayerType.STORAGE);
421+
List<String> curLayerStack = rscGrp.getSelectFilter() != null ?
422+
rscGrp.getSelectFilter().getLayerStack() : Collections.emptyList();
423+
if (CollectionUtils.isNotEmpty(curLayerStack)) {
424+
layers = curLayerStack.stream().map(LayerType::valueOf).collect(Collectors.toList());
425+
if (!layers.contains(LayerType.LUKS)) {
426+
layers.add(layers.size() - 1, LayerType.LUKS); // lowest layer is STORAGE
427+
}
428+
}
429+
return layers;
430+
} catch (ApiException e) {
431+
throw new CloudRuntimeException(
432+
String.format("Resource Group %s not found on Linstor cluster.", resourceGroup));
433+
}
434+
}
435+
394436
private String createResourceBase(
395-
String rscName, long sizeInBytes, String volName, String vmName, DevelopersApi api, String rscGrp) {
437+
String rscName, long sizeInBytes, String volName, String vmName,
438+
@Nullable Long passPhraseId, @Nullable byte[] passPhrase, DevelopersApi api, String rscGrp) {
396439
ResourceGroupSpawn rscGrpSpawn = new ResourceGroupSpawn();
397440
rscGrpSpawn.setResourceDefinitionName(rscName);
398441
rscGrpSpawn.addVolumeSizesItem(sizeInBytes / 1024);
442+
if (passPhraseId != null) {
443+
AutoSelectFilter asf = new AutoSelectFilter();
444+
List<LayerType> luksLayers = getEncryptedLayerList(api, rscGrp);
445+
asf.setLayerStack(luksLayers.stream().map(LayerType::toString).collect(Collectors.toList()));
446+
rscGrpSpawn.setSelectFilter(asf);
447+
if (passPhrase != null) {
448+
String utf8Passphrase = new String(passPhrase, StandardCharsets.UTF_8);
449+
rscGrpSpawn.setVolumePassphrases(Collections.singletonList(utf8Passphrase));
450+
}
451+
}
399452

400453
try
401454
{
@@ -420,7 +473,8 @@ private String createResource(VolumeInfo vol, StoragePoolVO storagePoolVO) {
420473

421474
final String rscName = LinstorUtil.RSC_PREFIX + vol.getUuid();
422475
String deviceName = createResourceBase(
423-
rscName, vol.getSize(), vol.getName(), vol.getAttachedVmName(), linstorApi, rscGrp);
476+
rscName, vol.getSize(), vol.getName(), vol.getAttachedVmName(), vol.getPassphraseId(), vol.getPassphrase(),
477+
linstorApi, rscGrp);
424478

425479
try
426480
{
@@ -461,6 +515,14 @@ private String cloneResource(long csCloneId, VolumeInfo volumeInfo, StoragePoolV
461515
s_logger.info("Clone resource definition " + cloneRes + " to " + rscName);
462516
ResourceDefinitionCloneRequest cloneRequest = new ResourceDefinitionCloneRequest();
463517
cloneRequest.setName(rscName);
518+
if (volumeInfo.getPassphraseId() != null) {
519+
List<LayerType> encryptionLayer = getEncryptedLayerList(linstorApi, getRscGrp(storagePoolVO));
520+
cloneRequest.setLayerList(encryptionLayer);
521+
if (volumeInfo.getPassphrase() != null) {
522+
String utf8Passphrase = new String(volumeInfo.getPassphrase(), StandardCharsets.UTF_8);
523+
cloneRequest.setVolumePassphrases(Collections.singletonList(utf8Passphrase));
524+
}
525+
}
464526
ResourceDefinitionCloneStarted cloneStarted = linstorApi.resourceDefinitionClone(
465527
cloneRes, cloneRequest);
466528

@@ -913,6 +975,8 @@ private Answer copyTemplate(DataObject srcData, DataObject dstData) {
913975
tInfo.getSize(),
914976
tInfo.getName(),
915977
"",
978+
null,
979+
null,
916980
api,
917981
getRscGrp(pool));
918982

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
package org.apache.cloudstack.storage.datastore.driver;
18+
19+
import com.linbit.linstor.api.ApiException;
20+
import com.linbit.linstor.api.DevelopersApi;
21+
import com.linbit.linstor.api.model.AutoSelectFilter;
22+
import com.linbit.linstor.api.model.LayerType;
23+
import com.linbit.linstor.api.model.ResourceGroup;
24+
25+
import java.util.Arrays;
26+
import java.util.Collections;
27+
import java.util.List;
28+
29+
import org.junit.Assert;
30+
import org.junit.Before;
31+
import org.junit.Test;
32+
import org.junit.runner.RunWith;
33+
import org.mockito.InjectMocks;
34+
import org.mockito.junit.MockitoJUnitRunner;
35+
36+
import static org.mockito.Mockito.mock;
37+
import static org.mockito.Mockito.when;
38+
39+
@RunWith(MockitoJUnitRunner.class)
40+
public class LinstorPrimaryDataStoreDriverImplTest {
41+
42+
private DevelopersApi api;
43+
44+
@InjectMocks
45+
private LinstorPrimaryDataStoreDriverImpl linstorPrimaryDataStoreDriver;
46+
47+
@Before
48+
public void setUp() {
49+
api = mock(DevelopersApi.class);
50+
}
51+
52+
@Test
53+
public void testGetEncryptedLayerList() throws ApiException {
54+
ResourceGroup dfltRscGrp = new ResourceGroup();
55+
dfltRscGrp.setName("DfltRscGrp");
56+
57+
ResourceGroup bCacheRscGrp = new ResourceGroup();
58+
bCacheRscGrp.setName("BcacheGrp");
59+
AutoSelectFilter asf = new AutoSelectFilter();
60+
asf.setLayerStack(Arrays.asList(LayerType.DRBD.name(), LayerType.BCACHE.name(), LayerType.STORAGE.name()));
61+
asf.setStoragePool("nvmePool");
62+
bCacheRscGrp.setSelectFilter(asf);
63+
64+
ResourceGroup encryptedGrp = new ResourceGroup();
65+
encryptedGrp.setName("EncryptedGrp");
66+
AutoSelectFilter asf2 = new AutoSelectFilter();
67+
asf2.setLayerStack(Arrays.asList(LayerType.DRBD.name(), LayerType.LUKS.name(), LayerType.STORAGE.name()));
68+
asf2.setStoragePool("ssdPool");
69+
encryptedGrp.setSelectFilter(asf2);
70+
71+
when(api.resourceGroupList(Collections.singletonList("DfltRscGrp"), Collections.emptyList(), null, null))
72+
.thenReturn(Collections.singletonList(dfltRscGrp));
73+
when(api.resourceGroupList(Collections.singletonList("BcacheGrp"), Collections.emptyList(), null, null))
74+
.thenReturn(Collections.singletonList(bCacheRscGrp));
75+
when(api.resourceGroupList(Collections.singletonList("EncryptedGrp"), Collections.emptyList(), null, null))
76+
.thenReturn(Collections.singletonList(encryptedGrp));
77+
78+
List<LayerType> layers = linstorPrimaryDataStoreDriver.getEncryptedLayerList(api, "DfltRscGrp");
79+
Assert.assertEquals(Arrays.asList(LayerType.DRBD, LayerType.LUKS, LayerType.STORAGE), layers);
80+
81+
layers = linstorPrimaryDataStoreDriver.getEncryptedLayerList(api, "BcacheGrp");
82+
Assert.assertEquals(Arrays.asList(LayerType.DRBD, LayerType.BCACHE, LayerType.LUKS, LayerType.STORAGE), layers);
83+
84+
layers = linstorPrimaryDataStoreDriver.getEncryptedLayerList(api, "EncryptedGrp");
85+
Assert.assertEquals(Arrays.asList(LayerType.DRBD, LayerType.LUKS, LayerType.STORAGE), layers);
86+
}
87+
}

0 commit comments

Comments
 (0)