From 2658e455edf4daefbe68bfa5a4223b333b3e0d3d Mon Sep 17 00:00:00 2001 From: Rene Peinthor Date: Wed, 3 Jun 2026 14:50:50 +0200 Subject: [PATCH 1/2] linstor: update java-linstor to 0.7.0 to support auth token api With Linstor 1.34.0 a new authentication mode is supported: * Bearer token To support that it had to be implemented in the java-linstor library and we need to store the auth token per storage pool. Also per default with this auth mode Linstor will run with HTTPS enabled, so we also have to support that. --- plugins/storage/volume/linstor/CHANGELOG.md | 6 + .../kvm/storage/LinstorStorageAdaptor.java | 27 ++++- .../kvm/storage/LinstorStoragePool.java | 8 +- .../LinstorPrimaryDataStoreDriverImpl.java | 41 ++++--- .../LinstorPrimaryDataStoreLifeCycleImpl.java | 40 ++++++- .../util/LinstorConfigChangeListener.java | 108 ++++++++++++++++++ .../util/LinstorConfigurationManager.java | 9 +- .../storage/datastore/util/LinstorUtil.java | 33 +++++- .../motion/LinstorDataMotionStrategy.java | 9 +- .../snapshot/LinstorVMSnapshotStrategy.java | 19 ++- .../spring-storage-volume-linstor-context.xml | 2 + pom.xml | 2 +- ui/public/locales/en.json | 4 + ui/src/views/infra/AddPrimaryStorage.vue | 19 ++- .../infra/zone/ZoneWizardAddResources.vue | 24 +++- .../views/infra/zone/ZoneWizardLaunchZone.vue | 4 + 16 files changed, 316 insertions(+), 39 deletions(-) create mode 100644 plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/util/LinstorConfigChangeListener.java diff --git a/plugins/storage/volume/linstor/CHANGELOG.md b/plugins/storage/volume/linstor/CHANGELOG.md index 1a3142e8c59b..070a752db04f 100644 --- a/plugins/storage/volume/linstor/CHANGELOG.md +++ b/plugins/storage/volume/linstor/CHANGELOG.md @@ -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 diff --git a/plugins/storage/volume/linstor/src/main/java/com/cloud/hypervisor/kvm/storage/LinstorStorageAdaptor.java b/plugins/storage/volume/linstor/src/main/java/com/cloud/hypervisor/kvm/storage/LinstorStorageAdaptor.java index 77953a32e63a..848bdc60d664 100644 --- a/plugins/storage/volume/linstor/src/main/java/com/cloud/hypervisor/kvm/storage/LinstorStorageAdaptor.java +++ b/plugins/storage/volume/linstor/src/main/java/com/cloud/hypervisor/kvm/storage/LinstorStorageAdaptor.java @@ -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; @@ -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; @@ -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); } @@ -166,7 +175,16 @@ public KVMStoragePool createStoragePool(String name, String host, int port, Stri Storage.StoragePoolType type, Map 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); @@ -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) { diff --git a/plugins/storage/volume/linstor/src/main/java/com/cloud/hypervisor/kvm/storage/LinstorStoragePool.java b/plugins/storage/volume/linstor/src/main/java/com/cloud/hypervisor/kvm/storage/LinstorStoragePool.java index 1bcfaa4ebf7f..8a02560fd464 100644 --- a/plugins/storage/volume/linstor/src/main/java/com/cloud/hypervisor/kvm/storage/LinstorStoragePool.java +++ b/plugins/storage/volume/linstor/src/main/java/com/cloud/hypervisor/kvm/storage/LinstorStoragePool.java @@ -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(); } @@ -213,6 +215,10 @@ public String getResourceGroup() { return _resourceGroup; } + public boolean isInsecureSsl() { + return _insecureSsl; + } + @Override public boolean isPoolSupportHA() { return true; diff --git a/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/driver/LinstorPrimaryDataStoreDriverImpl.java b/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/driver/LinstorPrimaryDataStoreDriverImpl.java index 3f06bee8ac83..672731fd07c9 100644 --- a/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/driver/LinstorPrimaryDataStoreDriverImpl.java +++ b/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/driver/LinstorPrimaryDataStoreDriverImpl.java @@ -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; @@ -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 { @@ -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 { @@ -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( @@ -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(); @@ -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); @@ -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(), @@ -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(); @@ -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; @@ -1147,7 +1154,7 @@ public void resize(DataObject data, AsyncCompletionCallback 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(); @@ -1221,7 +1228,7 @@ public void takeSnapshot(SnapshotInfo snapshotInfo, AsyncCompletionCallback 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 @@ -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 resources = api.viewResources( @@ -1327,7 +1336,9 @@ public Pair 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 sizePair = volumeStats.get(volumeKey); diff --git a/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/LinstorPrimaryDataStoreLifeCycleImpl.java b/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/LinstorPrimaryDataStoreLifeCycleImpl.java index fa9c1b71ff33..7c1330b9088e 100644 --- a/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/LinstorPrimaryDataStoreLifeCycleImpl.java +++ b/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/LinstorPrimaryDataStoreLifeCycleImpl.java @@ -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; @@ -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 @@ -65,6 +69,8 @@ public class LinstorPrimaryDataStoreLifeCycleImpl extends BasePrimaryDataStoreLi @Inject PrimaryDataStoreHelper dataStoreHelper; @Inject + private StoragePoolDetailsDao storagePoolDetailsDao; + @Inject private StoragePoolAutomation storagePoolAutomation; @Inject private CapacityManager _capacityMgr; @@ -122,7 +128,15 @@ public DataStore initialize(Map 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; } @@ -146,7 +160,18 @@ public DataStore initialize(Map 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."); } @@ -173,7 +198,16 @@ public DataStore initialize(Map 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) { diff --git a/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/util/LinstorConfigChangeListener.java b/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/util/LinstorConfigChangeListener.java new file mode 100644 index 000000000000..cec3fb68ea9f --- /dev/null +++ b/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/util/LinstorConfigChangeListener.java @@ -0,0 +1,108 @@ +// 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.storage.datastore.util; + +import java.util.Collections; +import java.util.Map; + +import javax.inject.Inject; +import javax.naming.ConfigurationException; + +import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.framework.messagebus.MessageBus; +import org.apache.cloudstack.framework.messagebus.MessageSubscriber; +import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; +import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import com.cloud.event.EventTypes; +import com.cloud.host.Host; +import com.cloud.host.dao.HostDao; +import com.cloud.storage.Storage; +import com.cloud.storage.StorageManager; +import com.cloud.storage.dao.StoragePoolHostDao; +import com.cloud.utils.Ternary; +import com.cloud.utils.component.ManagerBase; + +/** + * Management-server only component. Per-pool Linstor settings that the agent needs (the insecure-TLS + * flag) are delivered to the agent inside the storage pool details of a ModifyStoragePoolCommand and + * then cached in the agent's LinstorStoragePool. A dynamic {@code updateConfiguration} only updates the + * database and the management server's own config cache; it does not refresh the agent. This listener + * reacts to such changes and re-pushes the pool details to every connected host so the cached pool is + * rebuilt with the new value, without requiring a host reconnect. + */ +public class LinstorConfigChangeListener extends ManagerBase { + protected Logger logger = LogManager.getLogger(getClass()); + + @Inject + private MessageBus messageBus; + @Inject + private PrimaryDataStoreDao primaryDataStoreDao; + @Inject + private StoragePoolHostDao storagePoolHostDao; + @Inject + private HostDao hostDao; + @Inject + private StorageManager storageManager; + + @Override + public boolean configure(String name, Map params) throws ConfigurationException { + super.configure(name, params); + messageBus.subscribe(EventTypes.EVENT_CONFIGURATION_VALUE_EDIT, new ConfigValueChangeSubscriber()); + return true; + } + + private final class ConfigValueChangeSubscriber implements MessageSubscriber { + @Override + @SuppressWarnings("unchecked") + public void onPublishMessage(String senderAddress, String subject, Object args) { + if (!(args instanceof Ternary)) { + return; + } + final Ternary updated = (Ternary) args; + // Only the per-pool insecure-TLS flag has to reach the agent; the API token is read from + // the agent's local auth.json, so it never needs a re-push. + if (ConfigKey.Scope.StoragePool != updated.second() + || !LinstorConfigurationManager.InsecureSsl.key().equals(updated.first())) { + return; + } + + final Long poolId = updated.third(); + final StoragePoolVO pool = primaryDataStoreDao.findById(poolId); + if (pool == null || pool.getPoolType() != Storage.StoragePoolType.Linstor) { + return; + } + + logger.debug("Linstor: {} changed for storage pool {}, re-pushing pool details to connected hosts", + updated.first(), poolId); + for (Long hostId : storagePoolHostDao.findHostsConnectedToPools(Collections.singletonList(poolId))) { + final Host host = hostDao.findById(hostId); + if (host == null) { + continue; + } + try { + storageManager.connectHostToSharedPool(host, poolId); + logger.debug("Linstor: re-pushed pool {} details to host {}", poolId, hostId); + } catch (Exception e) { + logger.warn("Linstor: failed to re-push pool {} details to host {}", poolId, hostId, e); + } + } + } + } +} diff --git a/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/util/LinstorConfigurationManager.java b/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/util/LinstorConfigurationManager.java index 85a0804dbab6..0292eef64a54 100644 --- a/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/util/LinstorConfigurationManager.java +++ b/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/util/LinstorConfigurationManager.java @@ -29,8 +29,15 @@ public class LinstorConfigurationManager implements Configurable "Cache time of volume stats for Linstor volumes. 0 to disable volume stats", false); + public static final ConfigKey ApiToken = new ConfigKey<>(String.class, "lin.auth.apitoken", "Secure", "", + "API token used to authenticate on the Controller", true, ConfigKey.Scope.StoragePool, null); + + public static final ConfigKey InsecureSsl = new ConfigKey<>(Boolean.class, "lin.ssl.insecure", "Advanced", "true", + "Allow self-signed/untrusted TLS certificates from the Linstor controller (disables certificate and hostname verification)", + true, ConfigKey.Scope.StoragePool, null); + public static final ConfigKey[] CONFIG_KEYS = new ConfigKey[] { - BackupSnapshots, VolumeStatsCacheTime + ApiToken, BackupSnapshots, InsecureSsl, VolumeStatsCacheTime }; @Override diff --git a/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/util/LinstorUtil.java b/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/util/LinstorUtil.java index 7c45493dddc4..67c070f84eb0 100644 --- a/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/util/LinstorUtil.java +++ b/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/util/LinstorUtil.java @@ -51,6 +51,7 @@ import java.util.stream.Collectors; import com.cloud.hypervisor.kvm.storage.KVMStoragePool; +import com.cloud.hypervisor.kvm.storage.LinstorStoragePool; import com.cloud.utils.Pair; import com.cloud.utils.exception.CloudRuntimeException; import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo; @@ -78,8 +79,25 @@ public class LinstorUtil { public static final String CLUSTER_DEFAULT_MAX_IOPS = "clusterDefaultMaxIops"; public static DevelopersApi getLinstorAPI(String linstorUrl) { + return getLinstorAPI(linstorUrl, null, false); + } + + public static DevelopersApi getLinstorAPI(String linstorUrl, String apiToken) { + return getLinstorAPI(linstorUrl, apiToken, false); + } + + public static DevelopersApi getLinstorAPI(String linstorUrl, String apiToken, boolean insecureSsl) { ApiClient client = new ApiClient(); client.setBasePath(linstorUrl); + // An explicit (non-empty) token wins; otherwise the client falls back to the auth.json file + // on the local host (used on the agent side). No token at all -> unauthenticated controller. + client.setAccessTokenWithFallback(apiToken); + // Trust self-signed/untrusted controller certificates when explicitly allowed (rebuilds the + // http client, so set this before discovering HTTPS). + if (insecureSsl) { + client.setInsecureSsl(); + } + client.discoverHttps(); return new DevelopersApi(client); } @@ -224,8 +242,8 @@ public static List getRscGroupStoragePools(DevelopersApi api, Strin ); } - public static long getCapacityBytes(String linstorUrl, String rscGroupName) { - DevelopersApi linstorApi = getLinstorAPI(linstorUrl); + public static long getCapacityBytes(String linstorUrl, String rscGroupName, String apiToken, boolean insecureSsl) { + DevelopersApi linstorApi = getLinstorAPI(linstorUrl, apiToken, insecureSsl); try { List storagePools = getRscGroupStoragePools(linstorApi, rscGroupName); @@ -239,8 +257,8 @@ public static long getCapacityBytes(String linstorUrl, String rscGroupName) { } } - public static Pair getStorageStats(String linstorUrl, String rscGroupName) { - DevelopersApi linstorApi = getLinstorAPI(linstorUrl); + public static Pair getStorageStats(String linstorUrl, String rscGroupName, String apiToken, boolean insecureSsl) { + DevelopersApi linstorApi = getLinstorAPI(linstorUrl, apiToken, insecureSsl); try { List storagePools = LinstorUtil.getRscGroupStoragePools(linstorApi, rscGroupName); @@ -505,7 +523,8 @@ public static boolean isRscDiskless(ResourceWithVolumes rsc) { * @return true if all resources are on a provider with zeroed blocks. */ public static boolean resourceSupportZeroBlocks(KVMStoragePool pool, String resName) { - final DevelopersApi api = getLinstorAPI(pool.getSourceHost()); + final boolean insecureSsl = pool instanceof LinstorStoragePool && ((LinstorStoragePool) pool).isInsecureSsl(); + final DevelopersApi api = getLinstorAPI(pool.getSourceHost(), null, insecureSsl); try { List resWithVols = api.viewResources( Collections.emptyList(), @@ -760,7 +779,9 @@ public static String createResource(VolumeInfo vol, StoragePoolVO storagePoolVO, public static String createResource(VolumeInfo vol, StoragePoolVO storagePoolVO, PrimaryDataStoreDao primaryDataStoreDao, boolean exactSize) { - DevelopersApi linstorApi = LinstorUtil.getLinstorAPI(storagePoolVO.getHostAddress()); + DevelopersApi linstorApi = LinstorUtil.getLinstorAPI(storagePoolVO.getHostAddress(), + LinstorConfigurationManager.ApiToken.valueIn(storagePoolVO.getId()), + Boolean.TRUE.equals(LinstorConfigurationManager.InsecureSsl.valueIn(storagePoolVO.getId()))); final String rscGrp = getRscGrp(storagePoolVO); final String rscName = LinstorUtil.RSC_PREFIX + vol.getUuid(); diff --git a/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/motion/LinstorDataMotionStrategy.java b/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/motion/LinstorDataMotionStrategy.java index cab2820f09ae..f64837e4832e 100644 --- a/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/motion/LinstorDataMotionStrategy.java +++ b/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/motion/LinstorDataMotionStrategy.java @@ -70,6 +70,7 @@ import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreDao; 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.commons.collections.CollectionUtils; import org.apache.commons.collections.MapUtils; @@ -170,7 +171,9 @@ private VolumeVO createNewVolumeVO(Volume volume, StoragePoolVO storagePoolVO) { private void removeExactSizeProperty(VolumeInfo volumeInfo) { StoragePoolVO destStoragePool = _storagePool.findById(volumeInfo.getDataStore().getId()); - DevelopersApi api = LinstorUtil.getLinstorAPI(destStoragePool.getHostAddress()); + DevelopersApi api = LinstorUtil.getLinstorAPI(destStoragePool.getHostAddress(), + LinstorConfigurationManager.ApiToken.valueIn(destStoragePool.getId()), + Boolean.TRUE.equals(LinstorConfigurationManager.InsecureSsl.valueIn(destStoragePool.getId()))); ResourceDefinitionModify rdm = new ResourceDefinitionModify(); rdm.setDeleteProps(Collections.singletonList(LinstorUtil.LIN_PROP_DRBDOPT_EXACT_SIZE)); @@ -290,7 +293,9 @@ private void handlePostMigration(boolean success, Map sr private boolean needsExactSizeProp(VolumeInfo srcVolumeInfo) { StoragePoolVO srcStoragePool = _storagePool.findById(srcVolumeInfo.getDataStore().getId()); if (srcStoragePool.getPoolType() == Storage.StoragePoolType.Linstor) { - DevelopersApi api = LinstorUtil.getLinstorAPI(srcStoragePool.getHostAddress()); + DevelopersApi api = LinstorUtil.getLinstorAPI(srcStoragePool.getHostAddress(), + LinstorConfigurationManager.ApiToken.valueIn(srcStoragePool.getId()), + Boolean.TRUE.equals(LinstorConfigurationManager.InsecureSsl.valueIn(srcStoragePool.getId()))); String rscName = LinstorUtil.RSC_PREFIX + srcVolumeInfo.getPath(); try { diff --git a/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/snapshot/LinstorVMSnapshotStrategy.java b/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/snapshot/LinstorVMSnapshotStrategy.java index 4e4c882ae808..d0f82484694b 100644 --- a/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/snapshot/LinstorVMSnapshotStrategy.java +++ b/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/snapshot/LinstorVMSnapshotStrategy.java @@ -23,6 +23,7 @@ import com.linbit.linstor.api.model.ApiCallRcList; import com.linbit.linstor.api.model.CreateMultiSnapshotRequest; import com.linbit.linstor.api.model.Snapshot; +import com.linbit.linstor.api.model.SnapshotRollback; import javax.inject.Inject; @@ -49,6 +50,7 @@ import org.apache.cloudstack.engine.subsystem.api.storage.StrategyPriority; import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; 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.to.VolumeObjectTO; import org.apache.cloudstack.storage.vmsnapshot.DefaultVMSnapshotStrategy; @@ -137,7 +139,10 @@ public VMSnapshot takeVMSnapshot(VMSnapshot vmSnapshot) { try { final List volumeTOs = _vmSnapshotHelper.getVolumeTOList(userVm.getId()); final StoragePoolVO storagePool = _storagePoolDao.findById(volumeTOs.get(0).getPoolId()); - final DevelopersApi api = LinstorUtil.getLinstorAPI(storagePool.getHostAddress()); + final DevelopersApi api = LinstorUtil.getLinstorAPI( + storagePool.getHostAddress(), + LinstorConfigurationManager.ApiToken.valueIn(storagePool.getId()), + Boolean.TRUE.equals(LinstorConfigurationManager.InsecureSsl.valueIn(storagePool.getId()))); long prev_chain_size = 0; long virtual_size = 0; @@ -235,7 +240,10 @@ public boolean deleteVMSnapshot(VMSnapshot vmSnapshot) { List volumeTOs = _vmSnapshotHelper.getVolumeTOList(vmSnapshot.getVmId()); final StoragePoolVO storagePool = _storagePoolDao.findById(volumeTOs.get(0).getPoolId()); - final DevelopersApi api = LinstorUtil.getLinstorAPI(storagePool.getHostAddress()); + final DevelopersApi api = LinstorUtil.getLinstorAPI( + storagePool.getHostAddress(), + LinstorConfigurationManager.ApiToken.valueIn(storagePool.getId()), + Boolean.TRUE.equals(LinstorConfigurationManager.InsecureSsl.valueIn(storagePool.getId()))); final String snapshotName = vmSnapshotVO.getName(); final List failedToDelete = new ArrayList<>(); @@ -272,7 +280,7 @@ public boolean deleteVMSnapshot(VMSnapshot vmSnapshot) { private String linstorRevertSnapshot(final DevelopersApi api, final String rscName, final String snapshotName) { String resultMsg = null; try { - ApiCallRcList answers = api.resourceSnapshotRollback(rscName, snapshotName); + ApiCallRcList answers = api.resourceSnapshotRollback(rscName, snapshotName, new SnapshotRollback()); if (answers.hasError()) { resultMsg = LinstorUtil.getBestErrorMessage(answers); } @@ -289,7 +297,10 @@ private boolean revertVMSnapshotOperation(VMSnapshot vmSnapshot, long userVmId) List volumeTOs = _vmSnapshotHelper.getVolumeTOList(userVmId); final StoragePoolVO storagePool = _storagePoolDao.findById(volumeTOs.get(0).getPoolId()); - final DevelopersApi api = LinstorUtil.getLinstorAPI(storagePool.getHostAddress()); + final DevelopersApi api = LinstorUtil.getLinstorAPI( + storagePool.getHostAddress(), + LinstorConfigurationManager.ApiToken.valueIn(storagePool.getId()), + Boolean.TRUE.equals(LinstorConfigurationManager.InsecureSsl.valueIn(storagePool.getId()))); final String snapshotName = vmSnapshotVO.getName(); for (VolumeObjectTO volumeObjectTO : volumeTOs) { diff --git a/plugins/storage/volume/linstor/src/main/resources/META-INF/cloudstack/storage-volume-linstor/spring-storage-volume-linstor-context.xml b/plugins/storage/volume/linstor/src/main/resources/META-INF/cloudstack/storage-volume-linstor/spring-storage-volume-linstor-context.xml index 88d1051c71e4..ff46be9352ac 100644 --- a/plugins/storage/volume/linstor/src/main/resources/META-INF/cloudstack/storage-volume-linstor/spring-storage-volume-linstor-context.xml +++ b/plugins/storage/volume/linstor/src/main/resources/META-INF/cloudstack/storage-volume-linstor/spring-storage-volume-linstor-context.xml @@ -33,6 +33,8 @@ class="org.apache.cloudstack.storage.snapshot.LinstorVMSnapshotStrategy" /> + diff --git a/pom.xml b/pom.xml index 8392a099b576..1c7ae233d789 100644 --- a/pom.xml +++ b/pom.xml @@ -173,7 +173,7 @@ 10.1 2.6.6 0.6.0 - 0.6.1 + 0.7.0 0.10.2 3.4.4_1 4.0.1 diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json index fe3678c9f37b..18b76f102d44 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -2164,6 +2164,8 @@ "label.routeripv6": "IPv6 address for the VR in this Network.", "label.routing.firewall": "IPv4 Routing Firewall", "label.resourcegroup": "Resource group", +"label.linstor.apitoken": "Controller API token", +"label.linstor.ssl.insecure": "Allow self-signed certificate", "label.routingmode": "Routing mode", "label.routing.policy": "Routing policy", "label.routing.policy.terms": "Routing policy terms", @@ -3625,6 +3627,8 @@ "message.launch.zone.hint": "Configure Network components and traffic including IP addresses.", "message.license.agreements.not.accepted": "License agreements not accepted.", "message.linstor.resourcegroup.description": "Linstor resource group to use for primary storage.", +"message.linstor.apitoken.description": "API token used to authenticate against the Linstor controller. Leave empty to use an auth.json file on the management server and hosts, or for an unauthenticated controller.", +"message.linstor.ssl.insecure.description": "Trust self-signed/untrusted TLS certificates from the Linstor controller (disables certificate and hostname verification).", "message.list.zone.vmware.datacenter.empty": "No VMware Datacenter exists in the selected Zone", "message.list.zone.vmware.hosts.empty": "No EXSi hosts were found in the selected Datacenter.\nAre the entered credentials correct?\n", "message.listnsp.not.return.providerid": "error: listNetworkServiceProviders API doesn't return VirtualRouter provider ID.", diff --git a/ui/src/views/infra/AddPrimaryStorage.vue b/ui/src/views/infra/AddPrimaryStorage.vue index d46396bbb3a5..a67c61ff4247 100644 --- a/ui/src/views/infra/AddPrimaryStorage.vue +++ b/ui/src/views/infra/AddPrimaryStorage.vue @@ -406,6 +406,18 @@ + + + + + + + +