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 @@ + + + + + + + +