Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions plugins/storage/volume/linstor/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ All notable changes to Linstor CloudStack plugin will be documented in this file
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [2026-06-03]

### Added

- Support Linstor bearer token authentication (Linstor 1.34.0) and HTTPS controller connections

## [2026-01-17]

### Added
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import com.cloud.storage.Storage;
import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.utils.script.Script;
import org.apache.cloudstack.storage.datastore.util.LinstorConfigurationManager;
import org.apache.cloudstack.storage.datastore.util.LinstorUtil;
import org.apache.cloudstack.utils.qemu.QemuImg;
import org.apache.cloudstack.utils.qemu.QemuImgException;
Expand All @@ -42,7 +43,6 @@
import com.linbit.linstor.api.ApiClient;
import com.linbit.linstor.api.ApiConsts;
import com.linbit.linstor.api.ApiException;
import com.linbit.linstor.api.Configuration;
import com.linbit.linstor.api.DevelopersApi;
import com.linbit.linstor.api.model.ApiCallRc;
import com.linbit.linstor.api.model.ApiCallRcList;
Expand Down Expand Up @@ -72,8 +72,17 @@ public class LinstorStorageAdaptor implements StorageAdaptor {
private final String localNodeName;

private DevelopersApi getLinstorAPI(KVMStoragePool pool) {
ApiClient client = Configuration.getDefaultApiClient();
// Use a fresh client per pool so a self-signed/insecure pool can't weaken the TLS settings
// of another pool sharing this agent.
ApiClient client = new ApiClient();
client.setBasePath(pool.getSourceHost());
// The agent has no access to the per-pool API token config; fall back to the auth.json file
// on the host (/var/lib/linstor.d/auth.json), or stay unauthenticated if it is absent.
client.setAccessTokenWithFallback(null);
if (pool instanceof LinstorStoragePool && ((LinstorStoragePool) pool).isInsecureSsl()) {
client.setInsecureSsl();
}
client.discoverHttps();
return new DevelopersApi(client);
}

Expand Down Expand Up @@ -166,7 +175,16 @@ public KVMStoragePool createStoragePool(String name, String host, int port, Stri
Storage.StoragePoolType type, Map<String, String> details, boolean isPrimaryStorage)
{
logger.debug("Linstor createStoragePool: name: '{}', host: '{}', path: {}, userinfo: {}", name, host, path, userInfo);
LinstorStoragePool storagePool = new LinstorStoragePool(name, host, port, userInfo, type, this);
// The management server ships the per-pool config in the details map; the controller TLS
// verification can be disabled here for self-signed certificates. The ConfigKey default is only
// applied on the management server (via valueIn()) and is NOT shipped in the details, so when the
// detail is absent we must fall back to that default here too - otherwise a pool without an
// explicit setting would verify the certificate on the agent while the MS thinks it is disabled.
final String insecureSslDetail = details != null ? details.get(LinstorConfigurationManager.InsecureSsl.key()) : null;
boolean insecureSsl = insecureSslDetail != null
? Boolean.parseBoolean(insecureSslDetail)
: Boolean.parseBoolean(LinstorConfigurationManager.InsecureSsl.defaultValue());
LinstorStoragePool storagePool = new LinstorStoragePool(name, host, port, userInfo, type, this, insecureSsl);

MapStorageUuidToStoragePool.put(name, storagePool);

Expand Down Expand Up @@ -742,7 +760,8 @@ public KVMPhysicalDisk createTemplateFromDirectDownloadFile(String templateFileP

public long getCapacity(LinstorStoragePool pool) {
final String rscGroupName = pool.getResourceGroup();
return LinstorUtil.getCapacityBytes(pool.getSourceHost(), rscGroupName);
// Agent side: no per-pool token config; fall back to the host's auth.json (or unauthenticated).
return LinstorUtil.getCapacityBytes(pool.getSourceHost(), rscGroupName, null, pool.isInsecureSsl());
}

public long getAvailable(LinstorStoragePool pool) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,16 +46,18 @@ public class LinstorStoragePool implements KVMStoragePool {
private final Storage.StoragePoolType _storagePoolType;
private final StorageAdaptor _storageAdaptor;
private final String _resourceGroup;
private final boolean _insecureSsl;
private final String localNodeName;

public LinstorStoragePool(String uuid, String host, int port, String resourceGroup,
Storage.StoragePoolType storagePoolType, StorageAdaptor storageAdaptor) {
Storage.StoragePoolType storagePoolType, StorageAdaptor storageAdaptor, boolean insecureSsl) {
_uuid = uuid;
_sourceHost = host;
_sourcePort = port;
_storagePoolType = storagePoolType;
_storageAdaptor = storageAdaptor;
_resourceGroup = resourceGroup;
_insecureSsl = insecureSsl;
localNodeName = getHostname();
}

Expand Down Expand Up @@ -213,6 +215,10 @@ public String getResourceGroup() {
return _resourceGroup;
}

public boolean isInsecureSsl() {
return _insecureSsl;
}

@Override
public boolean isPoolSupportHA() {
return true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import com.linbit.linstor.api.model.ResourceWithVolumes;
import com.linbit.linstor.api.model.Snapshot;
import com.linbit.linstor.api.model.SnapshotRestore;
import com.linbit.linstor.api.model.SnapshotRollback;
import com.linbit.linstor.api.model.VolumeDefinitionModify;

import javax.annotation.Nonnull;
Expand Down Expand Up @@ -214,9 +215,15 @@ private String getSnapshotName(String snapshotUuid) {
return LinstorUtil.RSC_PREFIX + snapshotUuid;
}

private DevelopersApi getLinstorAPI(StoragePool pool) {
return LinstorUtil.getLinstorAPI(pool.getHostAddress(),
LinstorConfigurationManager.ApiToken.valueIn(pool.getId()),
Boolean.TRUE.equals(LinstorConfigurationManager.InsecureSsl.valueIn(pool.getId())));
}

private void deleteResourceDefinition(StoragePoolVO storagePoolVO, String rscDefName)
{
DevelopersApi linstorApi = LinstorUtil.getLinstorAPI(storagePoolVO.getHostAddress());
DevelopersApi linstorApi = getLinstorAPI(storagePoolVO);

try
{
Expand All @@ -240,7 +247,7 @@ private void deleteResourceDefinition(StoragePoolVO storagePoolVO, String rscDef
private void deleteSnapshot(@Nonnull DataStore dataStore, @Nonnull String rscDefName, @Nonnull String snapshotName)
{
StoragePoolVO storagePool = _storagePoolDao.findById(dataStore.getId());
DevelopersApi linstorApi = LinstorUtil.getLinstorAPI(storagePool.getHostAddress());
DevelopersApi linstorApi = getLinstorAPI(storagePool);

try
{
Expand Down Expand Up @@ -411,7 +418,7 @@ private String cloneResource(long csCloneId, VolumeInfo volumeInfo, StoragePoolV
}
final String rscName = LinstorUtil.RSC_PREFIX + volumeInfo.getUuid();

final DevelopersApi linstorApi = LinstorUtil.getLinstorAPI(storagePoolVO.getHostAddress());
final DevelopersApi linstorApi = getLinstorAPI(storagePoolVO);

try {
ResourceDefinition templateRD = LinstorUtil.findResourceDefinition(
Expand Down Expand Up @@ -474,7 +481,7 @@ private ResourceDefinitionCreate createResourceDefinitionCreate(String rscName,

private String createResourceFromSnapshot(long csSnapshotId, String rscName, StoragePoolVO storagePoolVO) {
final String rscGrp = LinstorUtil.getRscGrp(storagePoolVO);
final DevelopersApi linstorApi = LinstorUtil.getLinstorAPI(storagePoolVO.getHostAddress());
final DevelopersApi linstorApi = getLinstorAPI(storagePoolVO);

SnapshotVO snapshotVO = _snapshotDao.findById(csSnapshotId);
String snapName = LinstorUtil.RSC_PREFIX + snapshotVO.getUuid();
Expand Down Expand Up @@ -672,14 +679,14 @@ private String revertSnapshotFromImageStore(

private String doRevertSnapshot(final SnapshotInfo snapshot, final VolumeInfo volumeInfo) {
final StoragePool pool = (StoragePool) volumeInfo.getDataStore();
final DevelopersApi linstorApi = LinstorUtil.getLinstorAPI(pool.getHostAddress());
final DevelopersApi linstorApi = getLinstorAPI(pool);
final String rscName = LinstorUtil.RSC_PREFIX + volumeInfo.getPath();
String resultMsg;
try {
if (snapshot.getDataStore().getRole() == DataStoreRole.Primary) {
final String snapName = LinstorUtil.RSC_PREFIX + snapshot.getUuid();

ApiCallRcList answers = linstorApi.resourceSnapshotRollback(rscName, snapName);
ApiCallRcList answers = linstorApi.resourceSnapshotRollback(rscName, snapName, new SnapshotRollback());
resultMsg = checkLinstorAnswers(answers);
} else if (snapshot.getDataStore().getRole() == DataStoreRole.Image) {
resultMsg = revertSnapshotFromImageStore(snapshot, volumeInfo, linstorApi, rscName);
Expand Down Expand Up @@ -913,7 +920,7 @@ private void updateTemplateSpoolRef(
private Answer copyTemplate(DataObject srcData, DataObject dstData) {
TemplateInfo tInfo = (TemplateInfo) dstData;
final StoragePoolVO pool = _storagePoolDao.findById(dstData.getDataStore().getId());
final DevelopersApi api = LinstorUtil.getLinstorAPI(pool.getHostAddress());
final DevelopersApi api = getLinstorAPI(pool);
final String rscName = LinstorUtil.RSC_PREFIX + dstData.getUuid();
boolean newCreated = LinstorUtil.createResourceBase(
LinstorUtil.RSC_PREFIX + dstData.getUuid(),
Expand Down Expand Up @@ -961,7 +968,7 @@ private Answer copyTemplate(DataObject srcData, DataObject dstData) {
private Answer copyVolume(DataObject srcData, DataObject dstData) {
VolumeInfo srcVolInfo = (VolumeInfo) srcData;
final StoragePoolVO pool = _storagePoolDao.findById(srcVolInfo.getDataStore().getId());
final DevelopersApi api = LinstorUtil.getLinstorAPI(pool.getHostAddress());
final DevelopersApi api = getLinstorAPI(pool);
final String rscName = LinstorUtil.RSC_PREFIX + srcVolInfo.getPath();

VolumeObjectTO to = (VolumeObjectTO) srcVolInfo.getTO();
Expand Down Expand Up @@ -1066,7 +1073,7 @@ protected Answer copySnapshot(DataObject srcData, DataObject destData) {
SnapshotObject snapshotObject = (SnapshotObject)srcData;
Boolean snapshotFullBackup = snapshotObject.getFullBackup();
final StoragePoolVO pool = _storagePoolDao.findById(srcData.getDataStore().getId());
final DevelopersApi api = LinstorUtil.getLinstorAPI(pool.getHostAddress());
final DevelopersApi api = getLinstorAPI(pool);
boolean fullSnapshot = true;
if (snapshotFullBackup != null) {
fullSnapshot = snapshotFullBackup;
Expand Down Expand Up @@ -1147,7 +1154,7 @@ public void resize(DataObject data, AsyncCompletionCallback<CreateCmdResult> cal
{
final VolumeObject vol = (VolumeObject) data;
final StoragePoolVO pool = _storagePoolDao.findById(data.getDataStore().getId());
final DevelopersApi api = LinstorUtil.getLinstorAPI(pool.getHostAddress());
final DevelopersApi api = getLinstorAPI(pool);
final ResizeVolumePayload resizeParameter = (ResizeVolumePayload) vol.getpayload();

final String rscName = LinstorUtil.RSC_PREFIX + vol.getPath();
Expand Down Expand Up @@ -1221,7 +1228,7 @@ public void takeSnapshot(SnapshotInfo snapshotInfo, AsyncCompletionCallback<Crea

long storagePoolId = volumeVO.getPoolId();
final StoragePoolVO storagePool = _storagePoolDao.findById(storagePoolId);
final DevelopersApi api = LinstorUtil.getLinstorAPI(storagePool.getHostAddress());
final DevelopersApi api = getLinstorAPI(storagePool);
final String rscName = LinstorUtil.RSC_PREFIX + volumeVO.getPath();

Snapshot snapshot = new Snapshot();
Expand Down Expand Up @@ -1265,7 +1272,9 @@ public boolean canProvideStorageStats() {
@Override
public Pair<Long, Long> getStorageStats(StoragePool storagePool) {
logger.debug(String.format("Requesting storage stats: %s", storagePool));
return LinstorUtil.getStorageStats(storagePool.getHostAddress(), LinstorUtil.getRscGrp(storagePool));
return LinstorUtil.getStorageStats(storagePool.getHostAddress(), LinstorUtil.getRscGrp(storagePool),
LinstorConfigurationManager.ApiToken.valueIn(storagePool.getId()),
Boolean.TRUE.equals(LinstorConfigurationManager.InsecureSsl.valueIn(storagePool.getId())));
}

@Override
Expand All @@ -1277,8 +1286,8 @@ public boolean canProvideVolumeStats() {
* Updates the cache map containing current allocated size data.
* @param linstorAddr Linstor cluster api address
*/
private void fillVolumeStatsCache(String linstorAddr) {
final DevelopersApi api = LinstorUtil.getLinstorAPI(linstorAddr);
private void fillVolumeStatsCache(String linstorAddr, String apiToken, boolean insecureSsl) {
final DevelopersApi api = LinstorUtil.getLinstorAPI(linstorAddr, apiToken, insecureSsl);
try {
logger.trace("Start volume stats cache update for " + linstorAddr);
List<ResourceWithVolumes> resources = api.viewResources(
Expand Down Expand Up @@ -1327,7 +1336,9 @@ public Pair<Long, Long> getVolumeStats(StoragePool storagePool, String volumeId)
long invalidateCacheTime = volumeStatsLastUpdate.getOrDefault(storagePool.getHostAddress(), 0L) +
LinstorConfigurationManager.VolumeStatsCacheTime.value() * 1000;
if (invalidateCacheTime < System.currentTimeMillis()) {
fillVolumeStatsCache(storagePool.getHostAddress());
fillVolumeStatsCache(storagePool.getHostAddress(),
LinstorConfigurationManager.ApiToken.valueIn(storagePool.getId()),
Boolean.TRUE.equals(LinstorConfigurationManager.InsecureSsl.valueIn(storagePool.getId())));
}
String volumeKey = linstorAddr + "/" + LinstorUtil.RSC_PREFIX + volumeId;
Pair<Long, Long> sizePair = volumeStats.get(volumeKey);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
import com.cloud.storage.StoragePool;
import com.cloud.storage.StoragePoolAutomation;
import com.cloud.storage.dao.StoragePoolAndAccessGroupMapDao;
import com.cloud.utils.crypt.DBEncryptionUtil;
import com.cloud.utils.exception.CloudRuntimeException;
import org.apache.cloudstack.engine.subsystem.api.storage.ClusterScope;
import org.apache.cloudstack.engine.subsystem.api.storage.DataStore;
Expand All @@ -49,9 +50,12 @@
import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStoreParameters;
import org.apache.cloudstack.engine.subsystem.api.storage.ZoneScope;
import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao;
import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailsDao;
import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
import org.apache.cloudstack.storage.datastore.util.LinstorConfigurationManager;
import org.apache.cloudstack.storage.datastore.util.LinstorUtil;
import org.apache.cloudstack.storage.volume.datastore.PrimaryDataStoreHelper;
import org.apache.commons.lang3.StringUtils;

public class LinstorPrimaryDataStoreLifeCycleImpl extends BasePrimaryDataStoreLifeCycleImpl implements PrimaryDataStoreLifeCycle {
@Inject
Expand All @@ -65,6 +69,8 @@ public class LinstorPrimaryDataStoreLifeCycleImpl extends BasePrimaryDataStoreLi
@Inject
PrimaryDataStoreHelper dataStoreHelper;
@Inject
private StoragePoolDetailsDao storagePoolDetailsDao;
@Inject
private StoragePoolAutomation storagePoolAutomation;
@Inject
private CapacityManager _capacityMgr;
Expand Down Expand Up @@ -122,7 +128,15 @@ public DataStore initialize(Map<String, Object> dsInfos) {
}
}

if (!url.contains("://")) {
// The linstor client accepts the "linstor://" (HTTP, default port 3370) and "linstor+ssl://"
// (HTTPS, default port 3371) scheme aliases, but java.net.URL only understands http/https.
// Normalize the aliases so the URL parses and is stored in a scheme the client connects with.
final String lowerUrl = url.toLowerCase();
if (lowerUrl.startsWith("linstor+ssl://")) {
url = "https://" + url.substring("linstor+ssl://".length());
} else if (lowerUrl.startsWith("linstor://")) {
url = "http://" + url.substring("linstor://".length());
} else if (!url.contains("://")) {
url = "http://" + url;
}

Expand All @@ -146,7 +160,18 @@ public DataStore initialize(Map<String, Object> dsInfos) {
throw new IllegalArgumentException("Linstor controller URL is not valid: " + e);
}

long capacityBytes = LinstorUtil.getCapacityBytes(url, resourceGroup);
// The per-pool token config does not exist yet at creation time, so the token (when the
// controller requires one) may be supplied directly in the add-pool details. Pull it out of
// the details map so it is not persisted in clear text by the generic details handling, use it
// for the initial capacity probe, and store it (encrypted) as the per-pool config once the pool
// exists (below). When no token is given we fall back to the management server's auth.json (or
// an unauthenticated controller). The self-signed/insecure TLS flag is read the same way.
final String apiToken = details != null ? details.remove(LinstorConfigurationManager.ApiToken.key()) : null;
final String insecureSslDetail = details != null ? details.get(LinstorConfigurationManager.InsecureSsl.key()) : null;
final boolean insecureSsl = insecureSslDetail != null
? Boolean.parseBoolean(insecureSslDetail)
: Boolean.parseBoolean(LinstorConfigurationManager.InsecureSsl.defaultValue());
long capacityBytes = LinstorUtil.getCapacityBytes(url, resourceGroup, apiToken, insecureSsl);
if (capacityBytes <= 0) {
throw new IllegalArgumentException("'capacityBytes' must be present and greater than 0.");
}
Expand All @@ -173,7 +198,16 @@ public DataStore initialize(Map<String, Object> dsInfos) {
parameters.setDetails(details);
parameters.setUserInfo(resourceGroup);

return dataStoreHelper.createPrimaryDataStore(parameters);
DataStore dataStore = dataStoreHelper.createPrimaryDataStore(parameters);

if (dataStore != null && StringUtils.isNotEmpty(apiToken)) {
// lin.auth.apitoken is a "Secure" config, so its value must be stored encrypted for
// ConfigKey.valueIn() to be able to decrypt it on read.
storagePoolDetailsDao.addDetail(dataStore.getId(),
LinstorConfigurationManager.ApiToken.key(), DBEncryptionUtil.encrypt(apiToken), false);
}

return dataStore;
}

protected boolean createStoragePool(Host host, StoragePool pool) {
Expand Down
Loading
Loading