Skip to content

Commit cf061ca

Browse files
rp-dhslove
authored andcommitted
linstor: Fix using multiple primary storage with same linstor-controller (apache#10280)
1 parent 53f93b5 commit cf061ca

File tree

3 files changed

+230
-14
lines changed

3 files changed

+230
-14
lines changed

plugins/storage/volume/linstor/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ All notable changes to Linstor CloudStack plugin will be documented in this file
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [2025-01-27]
9+
10+
### Fixed
11+
12+
- Use of multiple primary storages on the same linstor controller
13+
814
## [2025-01-20]
915

1016
### Fixed

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

Lines changed: 109 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,11 @@
6161
import com.linbit.linstor.api.model.VolumeDefinition;
6262

6363
import java.io.File;
64+
import java.io.FileInputStream;
65+
import java.io.IOException;
66+
import java.nio.file.Files;
67+
import java.nio.file.Path;
68+
import java.nio.file.Paths;
6469

6570
public class LinstorStorageAdaptor implements StorageAdaptor {
6671
protected Logger logger = LogManager.getLogger(getClass());
@@ -202,10 +207,10 @@ public KVMPhysicalDisk createPhysicalDisk(String name, KVMStoragePool pool, Qemu
202207
final DevelopersApi api = getLinstorAPI(pool);
203208

204209
try {
205-
List<ResourceDefinition> definitionList = api.resourceDefinitionList(
206-
Collections.singletonList(rscName), null, null, null);
210+
ResourceDefinition resourceDefinition = LinstorUtil.findResourceDefinition(
211+
api, rscName, lpool.getResourceGroup());
207212

208-
if (definitionList.isEmpty()) {
213+
if (resourceDefinition == null) {
209214
ResourceGroupSpawn rgSpawn = new ResourceGroupSpawn();
210215
rgSpawn.setResourceDefinitionName(rscName);
211216
rgSpawn.addVolumeSizesItem(size / 1024); // linstor uses KiB
@@ -215,22 +220,28 @@ public KVMPhysicalDisk createPhysicalDisk(String name, KVMStoragePool pool, Qemu
215220
handleLinstorApiAnswers(answers, "Linstor: Unable to spawn resource.");
216221
}
217222

223+
String foundRscName = resourceDefinition != null ? resourceDefinition.getName() : rscName;
224+
218225
// query linstor for the device path
219226
List<ResourceWithVolumes> resources = api.viewResources(
220227
Collections.emptyList(),
221-
Collections.singletonList(rscName),
228+
Collections.singletonList(foundRscName),
222229
Collections.emptyList(),
223230
null,
224231
null,
225232
null);
226233

227-
makeResourceAvailable(api, rscName, false);
234+
makeResourceAvailable(api, foundRscName, false);
228235

229236
if (!resources.isEmpty() && !resources.get(0).getVolumes().isEmpty()) {
230237
final String devPath = resources.get(0).getVolumes().get(0).getDevicePath();
231238
logger.info("Linstor: Created drbd device: " + devPath);
232239
final KVMPhysicalDisk kvmDisk = new KVMPhysicalDisk(devPath, name, pool);
233240
kvmDisk.setFormat(QemuImg.PhysicalDiskFormat.RAW);
241+
long allocatedKib = resources.get(0).getVolumes().get(0).getAllocatedSizeKib() != null ?
242+
resources.get(0).getVolumes().get(0).getAllocatedSizeKib() : 0;
243+
kvmDisk.setSize(allocatedKib >= 0 ? allocatedKib * 1024 : 0);
244+
kvmDisk.setVirtualSize(size);
234245
return kvmDisk;
235246
} else {
236247
logger.error("Linstor: viewResources didn't return resources or volumes.");
@@ -473,21 +484,56 @@ public boolean disconnectPhysicalDiskByPath(String localPath)
473484
return false;
474485
}
475486

487+
/**
488+
* Decrements the aux property key for template resource and deletes or just deletes if not template resource.
489+
* @param api
490+
* @param rscName
491+
* @param rscGrpName
492+
* @return
493+
* @throws ApiException
494+
*/
495+
private boolean deRefOrDeleteResource(DevelopersApi api, String rscName, String rscGrpName) throws ApiException {
496+
boolean deleted = false;
497+
List<ResourceDefinition> existingRDs = LinstorUtil.getRDListStartingWith(api, rscName);
498+
for (ResourceDefinition rd : existingRDs) {
499+
int expectedProps = 0; // if it is a non template resource, we don't expect any _cs-template-for- prop
500+
String propKey = LinstorUtil.getTemplateForAuxPropKey(rscGrpName);
501+
if (rd.getProps().containsKey(propKey)) {
502+
ResourceDefinitionModify rdm = new ResourceDefinitionModify();
503+
rdm.deleteProps(Collections.singletonList(propKey));
504+
api.resourceDefinitionModify(rd.getName(), rdm);
505+
expectedProps = 1;
506+
}
507+
508+
// if there is only one template-for property left for templates, the template isn't needed anymore
509+
// or if it isn't a template anyway, it will not have this Aux property
510+
// _cs-template-for- properties work like a ref-count.
511+
if (rd.getProps().keySet().stream()
512+
.filter(key -> key.startsWith("Aux/" + LinstorUtil.CS_TEMPLATE_FOR_PREFIX))
513+
.count() == expectedProps) {
514+
ApiCallRcList answers = api.resourceDefinitionDelete(rd.getName());
515+
checkLinstorAnswersThrow(answers);
516+
deleted = true;
517+
}
518+
}
519+
return deleted;
520+
}
521+
476522
@Override
477523
public boolean deletePhysicalDisk(String name, KVMStoragePool pool, Storage.ImageFormat format)
478524
{
479525
logger.debug("Linstor: deletePhysicalDisk " + name);
480526
final DevelopersApi api = getLinstorAPI(pool);
527+
final String rscName = getLinstorRscName(name);
528+
final LinstorStoragePool linstorPool = (LinstorStoragePool) pool;
529+
String rscGrpName = linstorPool.getResourceGroup();
481530

482531
try {
483-
final String rscName = getLinstorRscName(name);
484-
logger.debug("Linstor: delete resource definition " + rscName);
485-
ApiCallRcList answers = api.resourceDefinitionDelete(rscName);
486-
handleLinstorApiAnswers(answers, "Linstor: Unable to delete resource definition " + rscName);
532+
return deRefOrDeleteResource(api, rscName, rscGrpName);
487533
} catch (ApiException apiEx) {
534+
logger.error("Linstor: ApiEx - " + apiEx.getMessage());
488535
throw new CloudRuntimeException(apiEx.getBestMessage(), apiEx);
489536
}
490-
return true;
491537
}
492538

493539
@Override
@@ -561,6 +607,56 @@ private boolean resourceSupportZeroBlocks(KVMStoragePool destPool, String resNam
561607
return false;
562608
}
563609

610+
/**
611+
* Checks if the given disk is the SystemVM template, by checking its properties file in the same directory.
612+
* The initial systemvm template resource isn't created on the management server, but
613+
* we now need to know if the systemvm template is used, while copying.
614+
* @param disk
615+
* @return True if it is the systemvm template disk, else false.
616+
*/
617+
private static boolean isSystemTemplate(KVMPhysicalDisk disk) {
618+
Path diskPath = Paths.get(disk.getPath());
619+
Path propFile = diskPath.getParent().resolve("template.properties");
620+
if (Files.exists(propFile)) {
621+
java.util.Properties templateProps = new java.util.Properties();
622+
try {
623+
templateProps.load(new FileInputStream(propFile.toFile()));
624+
String desc = templateProps.getProperty("description");
625+
if (desc.startsWith("SystemVM Template")) {
626+
return true;
627+
}
628+
} catch (IOException e) {
629+
return false;
630+
}
631+
}
632+
return false;
633+
}
634+
635+
/**
636+
* Conditionally sets the correct aux properties for templates or basic resources.
637+
* @param api
638+
* @param srcDisk
639+
* @param destPool
640+
* @param name
641+
*/
642+
private void setRscDfnAuxProperties(
643+
DevelopersApi api, KVMPhysicalDisk srcDisk, KVMStoragePool destPool, String name) {
644+
// if it is the initial systemvm disk copy, we need to apply the _cs-template-for property.
645+
if (isSystemTemplate(srcDisk)) {
646+
applyAuxProps(api, name, "SystemVM Template", null);
647+
LinstorStoragePool linPool = (LinstorStoragePool) destPool;
648+
final String rscName = getLinstorRscName(name);
649+
try {
650+
LinstorUtil.setAuxTemplateForProperty(api, rscName, linPool.getResourceGroup());
651+
} catch (ApiException apiExc) {
652+
logger.error("Error setting aux template for property for {}", rscName);
653+
logLinstorAnswers(apiExc.getApiCallRcList());
654+
}
655+
} else {
656+
applyAuxProps(api, name, srcDisk.getDispName(), srcDisk.getVmName());
657+
}
658+
}
659+
564660
@Override
565661
public KVMPhysicalDisk copyPhysicalDisk(KVMPhysicalDisk disk, String name, KVMStoragePool destPools, int timeout, byte[] srcPassphrase, byte[] destPassphrase, Storage.ProvisioningType provisioningType)
566662
{
@@ -574,15 +670,14 @@ public KVMPhysicalDisk copyPhysicalDisk(KVMPhysicalDisk disk, String name, KVMSt
574670
name, QemuImg.PhysicalDiskFormat.RAW, provisioningType, disk.getVirtualSize(), null);
575671

576672
final DevelopersApi api = getLinstorAPI(destPools);
577-
applyAuxProps(api, name, disk.getDispName(), disk.getVmName());
673+
setRscDfnAuxProperties(api, disk, destPools, name);
578674

579675
logger.debug("Linstor.copyPhysicalDisk: dstPath: {}", dstDisk.getPath());
580676
final QemuImgFile destFile = new QemuImgFile(dstDisk.getPath());
581677
destFile.setFormat(dstDisk.getFormat());
582678
destFile.setSize(disk.getVirtualSize());
583679

584-
boolean zeroedDevice = resourceSupportZeroBlocks(destPools, LinstorUtil.RSC_PREFIX + name);
585-
680+
boolean zeroedDevice = resourceSupportZeroBlocks(destPools, getLinstorRscName(name));
586681
try {
587682
final QemuImg qemu = new QemuImg(timeout, zeroedDevice, true);
588683
qemu.convert(srcFile, destFile);
@@ -735,4 +830,4 @@ public boolean isNodeOnline(LinstorStoragePool pool, String nodeName) {
735830
throw new CloudRuntimeException(apiEx.getBestMessage(), apiEx);
736831
}
737832
}
738-
}
833+
}

plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/util/LinstorUtil.java

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import com.linbit.linstor.api.model.Properties;
2727
import com.linbit.linstor.api.model.ProviderKind;
2828
import com.linbit.linstor.api.model.Resource;
29+
import com.linbit.linstor.api.model.ResourceDefinition;
2930
import com.linbit.linstor.api.model.ResourceDefinitionModify;
3031
import com.linbit.linstor.api.model.ResourceGroup;
3132
import com.linbit.linstor.api.model.ResourceWithVolumes;
@@ -37,8 +38,11 @@
3738
import java.util.Collection;
3839
import java.util.Collections;
3940
import java.util.List;
41+
import java.util.Map;
42+
import java.util.Optional;
4043
import java.util.stream.Collectors;
4144

45+
import com.cloud.utils.Pair;
4246
import com.cloud.utils.exception.CloudRuntimeException;
4347
import org.apache.logging.log4j.Logger;
4448
import org.apache.logging.log4j.LogManager;
@@ -49,6 +53,7 @@ public class LinstorUtil {
4953
public final static String PROVIDER_NAME = "Linstor";
5054
public static final String RSC_PREFIX = "cs-";
5155
public static final String RSC_GROUP = "resourceGroup";
56+
public static final String CS_TEMPLATE_FOR_PREFIX = "_cs-template-for-";
5257

5358
public static final String TEMP_VOLUME_ID = "tempVolumeId";
5459

@@ -288,4 +293,114 @@ public static ApiCallRcList applyAuxProps(DevelopersApi api, String rscName, Str
288293
}
289294
return answers;
290295
}
296+
297+
/**
298+
* Returns all resource definitions that start with the given `startWith` name.
299+
* @param api
300+
* @param startWith startWith String
301+
* @return a List with all ResourceDefinition starting with `startWith`
302+
* @throws ApiException
303+
*/
304+
public static List<ResourceDefinition> getRDListStartingWith(DevelopersApi api, String startWith)
305+
throws ApiException
306+
{
307+
List<ResourceDefinition> rscDfns = api.resourceDefinitionList(null, null, null, null);
308+
309+
return rscDfns.stream()
310+
.filter(rscDfn -> rscDfn.getName().toLowerCase().startsWith(startWith.toLowerCase()))
311+
.collect(Collectors.toList());
312+
}
313+
314+
/**
315+
* Returns a pair list of resource-definitions with ther 1:1 mapped resource-group objects that start with the
316+
* resource name `startWith`
317+
* @param api
318+
* @param startWith
319+
* @return
320+
* @throws ApiException
321+
*/
322+
public static List<Pair<ResourceDefinition, ResourceGroup>> getRDAndRGListStartingWith(DevelopersApi api, String startWith)
323+
throws ApiException
324+
{
325+
List<ResourceDefinition> foundRDs = getRDListStartingWith(api, startWith);
326+
327+
List<String> rscGrpStrings = foundRDs.stream()
328+
.map(ResourceDefinition::getResourceGroupName)
329+
.collect(Collectors.toList());
330+
331+
Map<String, ResourceGroup> rscGrps = api.resourceGroupList(rscGrpStrings, null, null, null).stream()
332+
.collect(Collectors.toMap(ResourceGroup::getName, rscGrp -> rscGrp));
333+
334+
return foundRDs.stream()
335+
.map(rd -> new Pair<>(rd, rscGrps.get(rd.getResourceGroupName())))
336+
.collect(Collectors.toList());
337+
}
338+
339+
/**
340+
* The full name of template-for aux property key.
341+
* @param rscGrpName
342+
* @return
343+
*/
344+
public static String getTemplateForAuxPropKey(String rscGrpName) {
345+
return String.format("Aux/%s%s", CS_TEMPLATE_FOR_PREFIX, rscGrpName);
346+
}
347+
348+
/**
349+
* Template resource should have a _cs-template-for-... property, that indicates to which resource-group
350+
* this template belongs, it works like a refcount to keep it alive if there are still such properties on the
351+
* template resource. That methods set the correct property on the given resource.
352+
* @param api
353+
* @param rscName Resource name to set the property.
354+
* @param rscGrpName Resource group this template should belong too.
355+
* @throws ApiException
356+
*/
357+
public static void setAuxTemplateForProperty(DevelopersApi api, String rscName, String rscGrpName)
358+
throws ApiException
359+
{
360+
ResourceDefinitionModify rdm = new ResourceDefinitionModify();
361+
Properties props = new Properties();
362+
String propKey = LinstorUtil.getTemplateForAuxPropKey(rscGrpName);
363+
props.put(propKey, "true");
364+
rdm.setOverrideProps(props);
365+
ApiCallRcList answers = api.resourceDefinitionModify(rscName, rdm);
366+
367+
if (answers.hasError()) {
368+
String bestError = LinstorUtil.getBestErrorMessage(answers);
369+
s_logger.error(String.format("Set %s on %s error: %s", propKey, rscName, bestError));
370+
throw new CloudRuntimeException(bestError);
371+
} else {
372+
s_logger.info(String.format("Set %s property on %s", propKey, rscName));
373+
}
374+
}
375+
376+
/**
377+
* Find the correct resource definition to clone from.
378+
* There could be multiple resource definitions for the same template, with the same prefix.
379+
* This method searches for which resource group the resource definition was intended and returns that.
380+
* If no exact resource definition could be found, we return the first with a similar name as a fallback.
381+
* If there is not even one with the correct prefix, we return null.
382+
* @param api
383+
* @param rscName
384+
* @param rscGrpName
385+
* @return The resource-definition to clone from, if no template and no match, return null.
386+
* @throws ApiException
387+
*/
388+
public static ResourceDefinition findResourceDefinition(DevelopersApi api, String rscName, String rscGrpName)
389+
throws ApiException {
390+
List<ResourceDefinition> rscDfns = api.resourceDefinitionList(null, null, null, null);
391+
392+
List<ResourceDefinition> rdsStartingWith = rscDfns.stream()
393+
.filter(rscDfn -> rscDfn.getName().toLowerCase().startsWith(rscName.toLowerCase()))
394+
.collect(Collectors.toList());
395+
396+
if (rdsStartingWith.isEmpty()) {
397+
return null;
398+
}
399+
400+
Optional<ResourceDefinition> rd = rdsStartingWith.stream()
401+
.filter(rscDfn -> rscDfn.getProps().containsKey(LinstorUtil.getTemplateForAuxPropKey(rscGrpName)))
402+
.findFirst();
403+
404+
return rd.orElseGet(() -> rdsStartingWith.get(0));
405+
}
291406
}

0 commit comments

Comments
 (0)