Merge remote-tracking branch 'apache/4.22'

This commit is contained in:
Wei Zhou 2026-07-07 10:19:50 +02:00
commit b3b9caddc1
No known key found for this signature in database
GPG Key ID: 1503DFE7C8226103
41 changed files with 799 additions and 175 deletions

View File

@ -1499,7 +1499,7 @@ public class ApiConstants {
}
public enum HostDetails {
all, capacity, events, stats, min;
all, capacity, core, events, stats, min;
}
public enum VMDetails {

View File

@ -201,6 +201,10 @@ public class HostResponse extends BaseResponseWithAnnotations {
@Param(description = "the virtual machine id for host type ConsoleProxy and SecondaryStorageVM", since = "4.21.0")
private String virtualMachineId;
@SerializedName("msid")
@Param(description = "(only for details=core) the msid of the host's management server")
private Long msId;
@SerializedName(ApiConstants.MANAGEMENT_SERVER_ID)
@Param(description = "The management server ID of the host")
private String managementServerId;
@ -492,6 +496,14 @@ public class HostResponse extends BaseResponseWithAnnotations {
this.virtualMachineId = virtualMachineId;
}
public Long getMsId() {
return msId;
}
public void setMsId(Long msId) {
this.msId = msId;
}
public void setManagementServerId(String managementServerId) {
this.managementServerId = managementServerId;
}

View File

@ -51,6 +51,8 @@ public interface VlanDao extends GenericDao<VlanVO, Long> {
List<VlanVO> listVlansByNetworkId(long networkId);
List<VlanVO> listVlansByNetworkIds(List<Long> networkIds);
List<VlanVO> listVlansByNetworkIdIncludingRemoved(long networkId);
List<VlanVO> listVlansByPhysicalNetworkId(long physicalNetworkId);

View File

@ -20,6 +20,7 @@ import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@ -27,6 +28,7 @@ import javax.inject.Inject;
import javax.naming.ConfigurationException;
import com.cloud.dc.VlanDetailsVO;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
@ -134,7 +136,7 @@ public class VlanDaoImpl extends GenericDaoBase<VlanVO, Long> implements VlanDao
ZoneTypeSearch.done();
NetworkVlanSearch = createSearchBuilder();
NetworkVlanSearch.and("networkId", NetworkVlanSearch.entity().getNetworkId(), SearchCriteria.Op.EQ);
NetworkVlanSearch.and("networkId", NetworkVlanSearch.entity().getNetworkId(), SearchCriteria.Op.IN);
NetworkVlanSearch.done();
PhysicalNetworkVlanSearch = createSearchBuilder();
@ -392,6 +394,16 @@ public class VlanDaoImpl extends GenericDaoBase<VlanVO, Long> implements VlanDao
return listBy(sc);
}
@Override
public List<VlanVO> listVlansByNetworkIds(List<Long> networkIds) {
if (CollectionUtils.isEmpty(networkIds)) {
return Collections.emptyList();
}
SearchCriteria<VlanVO> sc = NetworkVlanSearch.create();
sc.setParameters("networkId", networkIds.toArray());
return listBy(sc);
}
@Override public List<VlanVO> listVlansByNetworkIdIncludingRemoved(long networkId) {
SearchCriteria<VlanVO> sc = NetworkVlanSearch.create();
sc.setParameters("networkId", networkId);

View File

@ -99,6 +99,8 @@ public interface NicDao extends GenericDao<NicVO, Long> {
NicVO findByMacAddress(String macAddress, long networkId);
List<NicVO> listByMacAddresses(List<String> macAddresses);
NicVO findByNetworkIdAndMacAddressIncludingRemoved(long networkId, String mac);
List<NicVO> findNicsByIpv6GatewayIpv6CidrAndReserver(String ipv6Gateway, String ipv6Cidr, String reserverName);

View File

@ -18,12 +18,13 @@ package com.cloud.vm.dao;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.annotation.PostConstruct;
import javax.inject.Inject;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.stereotype.Component;
import com.cloud.utils.db.Filter;
@ -71,7 +72,7 @@ public class NicDaoImpl extends GenericDaoBase<NicVO, Long> implements NicDao {
AllFieldsSearch.and("strategy", AllFieldsSearch.entity().getReservationStrategy(), Op.EQ);
AllFieldsSearch.and("strategyNEQ", AllFieldsSearch.entity().getReservationStrategy(), Op.NEQ);
AllFieldsSearch.and("reserverName",AllFieldsSearch.entity().getReserver(),Op.EQ);
AllFieldsSearch.and("macAddress", AllFieldsSearch.entity().getMacAddress(), Op.EQ);
AllFieldsSearch.and("macAddress", AllFieldsSearch.entity().getMacAddress(), Op.IN);
AllFieldsSearch.and("deviceid", AllFieldsSearch.entity().getDeviceId(), Op.EQ);
AllFieldsSearch.and("ipv6Gateway", AllFieldsSearch.entity().getIPv6Gateway(), Op.EQ);
AllFieldsSearch.and("ipv6Cidr", AllFieldsSearch.entity().getIPv6Cidr(), Op.EQ);
@ -434,6 +435,16 @@ public class NicDaoImpl extends GenericDaoBase<NicVO, Long> implements NicDao {
return findOneBy(sc);
}
@Override
public List<NicVO> listByMacAddresses(List<String> macAddresses) {
if (CollectionUtils.isEmpty(macAddresses)) {
return Collections.emptyList();
}
SearchCriteria<NicVO> sc = AllFieldsSearch.create();
sc.setParameters("macAddress", macAddresses.toArray());
return listBy(sc);
}
@Override
public List<NicVO> findNicsByIpv6GatewayIpv6CidrAndReserver(String ipv6Gateway, String ipv6Cidr, String reserverName) {
SearchCriteria<NicVO> sc = AllFieldsSearch.create();

View File

@ -184,6 +184,7 @@ public class AsyncJobManagerImpl extends ManagerBase implements AsyncJobManager,
private volatile long _executionRunNumber = 1;
private final ScheduledExecutorService _heartbeatScheduler = Executors.newScheduledThreadPool(1, new NamedThreadFactory("AsyncJobMgr-Heartbeat"));
private final ExecutorService _eventBusPublisher = Executors.newSingleThreadExecutor(new NamedThreadFactory("AsyncJobMgr-EventBus"));
private ExecutorService _apiJobExecutor;
private ExecutorService _workerJobExecutor;
@ -1378,6 +1379,7 @@ public class AsyncJobManagerImpl extends ManagerBase implements AsyncJobManager,
@Override
public boolean stop() {
_heartbeatScheduler.shutdown();
_eventBusPublisher.shutdown();
_apiJobExecutor.shutdown();
_workerJobExecutor.shutdown();
return true;
@ -1397,8 +1399,26 @@ public class AsyncJobManagerImpl extends ManagerBase implements AsyncJobManager,
}
private void publishOnEventBus(AsyncJob job, String jobEvent) {
_messageBus.publish(null, AsyncJob.Topics.JOB_EVENT_PUBLISH, PublishScope.LOCAL,
new Pair<AsyncJob, String>(job, jobEvent));
try {
_eventBusPublisher.submit(new ManagedContextRunnable() {
@Override
protected void runInContext() {
publishJobEvent(job, jobEvent);
}
});
} catch (RejectedExecutionException e) {
logger.warn("Failed to publish async job event, event bus publisher is shut down", e);
}
}
private void publishJobEvent(AsyncJob job, String jobEvent) {
try {
_messageBus.publish(null, AsyncJob.Topics.JOB_EVENT_PUBLISH, PublishScope.LOCAL,
new Pair<>(job, jobEvent));
} catch (Throwable t) {
logger.warn("Failed to publish async job event on message bus. jobId={}, jobEvent={}",
job != null ? job.getId() : null, jobEvent, t);
}
}
@Override

View File

@ -55,7 +55,7 @@ public final class LibvirtCheckOnHostCommandWrapper extends CommandWrapper<Check
if (hasHeartBeat) {
return new CheckOnHostAnswer(command, true, "Heart is beating");
} else {
return new CheckOnHostAnswer(command, "Heart is not beating");
return new CheckOnHostAnswer(command, false, "Heart is not beating");
}
} catch (final InterruptedException e) {
return new CheckOnHostAnswer(command, "CheckOnHostCommand: can't get status of host: InterruptedException");

View File

@ -119,7 +119,7 @@ public final class LibvirtGetVmIpAddressCommandWrapper extends CommandWrapper<Ge
continue;
}
String device = parts[0];
String mac = parts[1];
String mac = parts[parts.length - 3];
if (found) {
if (!device.equals("-") || !mac.equals("-")) {
break;
@ -128,8 +128,8 @@ public final class LibvirtGetVmIpAddressCommandWrapper extends CommandWrapper<Ge
continue;
}
found = true;
String ipFamily = parts[2];
String ipPart = parts[3].split("/")[0];
String ipFamily = parts[parts.length - 2];
String ipPart = parts[parts.length - 1].split("/")[0];
if (ipFamily.equals("ipv4")) {
ipv4 = ipPart;
} else if (ipFamily.equals("ipv6")) {

View File

@ -24,7 +24,6 @@ import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@ -97,8 +96,7 @@ public class LibvirtStorageAdaptor implements StorageAdaptor {
public static final int RBD_FEATURES = RBD_FEATURE_LAYERING + RBD_FEATURE_EXCLUSIVE_LOCK + RBD_FEATURE_OBJECT_MAP + RBD_FEATURE_FAST_DIFF + RBD_FEATURE_DEEP_FLATTEN;
private int rbdOrder = 0; /* Order 0 means 4MB blocks (the default) */
private static final Set<StoragePoolType> poolTypesThatEnableCreateDiskFromTemplateBacking = new HashSet<>(Arrays.asList(StoragePoolType.NetworkFilesystem,
StoragePoolType.Filesystem));
private static final Set<StoragePoolType> QEMU_IMG_MANAGED_POOL_TYPES = Set.of(StoragePoolType.NetworkFilesystem, StoragePoolType.Filesystem, StoragePoolType.SharedMountPoint);
public LibvirtStorageAdaptor(StorageLayer storage) {
_storageLayer = storage;
@ -134,8 +132,8 @@ public class LibvirtStorageAdaptor implements StorageAdaptor {
String volumeDesc = String.format("volume [%s], with template backing [%s], in pool [%s] (%s), with size [%s] and encryption is %s", name, template.getName(), destPool.getUuid(),
destPool.getType(), size, passphrase != null && passphrase.length > 0);
if (!poolTypesThatEnableCreateDiskFromTemplateBacking.contains(destPool.getType())) {
logger.info(String.format("Skipping creation of %s due to pool type is none of the following types %s.", volumeDesc, poolTypesThatEnableCreateDiskFromTemplateBacking.stream()
if (!QEMU_IMG_MANAGED_POOL_TYPES.contains(destPool.getType())) {
logger.info(String.format("Skipping creation of %s due to pool type is none of the following types %s.", volumeDesc, QEMU_IMG_MANAGED_POOL_TYPES.stream()
.map(type -> type.toString()).collect(Collectors.joining(", "))));
return null;
@ -961,7 +959,7 @@ public class LibvirtStorageAdaptor implements StorageAdaptor {
* </ul>
* </li>
* <li>
* {@link StoragePoolType#NetworkFilesystem} and {@link StoragePoolType#Filesystem}
* {@link StoragePoolType#NetworkFilesystem}, {@link StoragePoolType#Filesystem} and {@link StoragePoolType#SharedMountPoint}
* <ul>
* <li>
* If the format is {@link PhysicalDiskFormat#QCOW2} or {@link PhysicalDiskFormat#RAW}, utilizes QemuImg to create the physical disk through the method
@ -992,7 +990,7 @@ public class LibvirtStorageAdaptor implements StorageAdaptor {
return (dataPool == null) ? createPhysicalDiskByLibVirt(name, pool, PhysicalDiskFormat.RAW, provisioningType, size) :
createPhysicalDiskByQemuImg(name, pool, PhysicalDiskFormat.RAW, provisioningType, size, passphrase);
} else if (StoragePoolType.NetworkFilesystem.equals(poolType) || StoragePoolType.Filesystem.equals(poolType)) {
} else if (QEMU_IMG_MANAGED_POOL_TYPES.contains(poolType)) {
switch (format) {
case QCOW2:
case RAW:
@ -1062,7 +1060,7 @@ public class LibvirtStorageAdaptor implements StorageAdaptor {
destFile.setFormat(format);
destFile.setSize(size);
Map<String, String> options = new HashMap<String, String>();
if (List.of(StoragePoolType.NetworkFilesystem, StoragePoolType.Filesystem).contains(pool.getType())) {
if (QEMU_IMG_MANAGED_POOL_TYPES.contains(pool.getType())) {
options.put(QemuImg.PREALLOCATION, QemuImg.PreallocationType.getPreallocationType(provisioningType).toString());
}

View File

@ -109,6 +109,7 @@ public class KVMHostActivityChecker extends AdapterBase implements ActivityCheck
}
Status hostStatusFromNeighbour = checkHostStatusWithNeighbourHosts(host);
logger.debug("{} status reported from itself: {} and neighbor: {}", host.toString(), hostStatusFromItself, hostStatusFromNeighbour);
Status hostStatus = hostStatusFromItself;
if (hostStatusFromNeighbour == Status.Up && (hostStatusFromItself == Status.Disconnected || hostStatusFromItself == Status.Down)) {
hostStatus = Status.Disconnected;

View File

@ -61,6 +61,7 @@ import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import com.cloud.agent.api.CheckOnHostAnswer;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
@ -3139,7 +3140,9 @@ public class LibvirtComputingResourceTest {
assertNotNull(wrapper);
final Answer answer = wrapper.execute(command, libvirtComputingResourceMock);
assertFalse(answer.getResult());
assertTrue(answer.getResult());
assertTrue(answer instanceof CheckOnHostAnswer);
assertFalse(((CheckOnHostAnswer)answer).isAlive());
verify(libvirtComputingResourceMock, times(1)).getMonitor();
}

View File

@ -52,6 +52,12 @@ public class LibvirtGetVmIpAddressCommandWrapperTest {
" net4 2e:9b:60:dc:49:30 N/A N/A\n" + //
" lxc5b7327203b6f 92:b2:77:0b:a9:20 N/A N/A\n";
private static String VIRSH_DOMIF_OUTPUT_WINDOWS = " Name MAC address Protocol Address\n" + //
"-------------------------------------------------------------------------------\n" + //
" Ethernet Instance 0 02:0c:02:f9:00:80 ipv4 192.168.0.10/24\n" + //
" Loopback Pseudo-Interface 1 ipv6 ::1/128\n" + //
" - - ipv4 127.0.0.1/8\n";
@Before
public void setUp() {
MockitoAnnotations.openMocks(this);
@ -118,7 +124,34 @@ public class LibvirtGetVmIpAddressCommandWrapperTest {
when(getVmIpAddressCommand.getVmNetworkCidr()).thenReturn("192.168.0.0/24");
when(getVmIpAddressCommand.getMacAddress()).thenReturn("02:0c:02:f9:00:80");
when(getVmIpAddressCommand.isWindows()).thenReturn(true);
when(Script.executePipedCommands(anyList(), anyLong())).thenReturn(new Pair<>(0, "192.168.0.10"));
when(Script.executePipedCommands(anyList(), anyLong())).thenReturn(new Pair<>(0, VIRSH_DOMIF_OUTPUT_WINDOWS));
Answer answer = commandWrapper.execute(getVmIpAddressCommand, libvirtComputingResource);
assertTrue(answer.getResult());
assertEquals("192.168.0.10", answer.getDetails());
} finally {
if (scriptMock != null)
scriptMock.close();
}
}
@Test
public void testExecuteWithWindowsVm2() {
LibvirtComputingResource libvirtComputingResource = mock(LibvirtComputingResource.class);
GetVmIpAddressCommand getVmIpAddressCommand = mock(GetVmIpAddressCommand.class);
LibvirtGetVmIpAddressCommandWrapper commandWrapper = new LibvirtGetVmIpAddressCommandWrapper();
MockedStatic<Script> scriptMock = null;
try {
scriptMock = mockStatic(Script.class);
when(getVmIpAddressCommand.getVmName()).thenReturn("validVmName");
when(getVmIpAddressCommand.getVmNetworkCidr()).thenReturn("192.168.0.0/24");
when(getVmIpAddressCommand.getMacAddress()).thenReturn("02:0c:02:f9:00:80");
when(getVmIpAddressCommand.isWindows()).thenReturn(true);
when(Script.executePipedCommands(anyList(), anyLong())).thenReturn(new Pair<>(0, "")).thenReturn(new Pair<>(0, "192.168.0.10"));
Answer answer = commandWrapper.execute(getVmIpAddressCommand, libvirtComputingResource);

View File

@ -5807,7 +5807,7 @@ public class VmwareResource extends ServerResourceBase implements StoragePoolRes
}
protected Answer execute(CheckOnHostCommand cmd) {
return new CheckOnHostAnswer(cmd, null, "Not Implmeneted");
return new CheckOnHostAnswer(cmd, null, "Not Implemented");
}
protected Answer execute(ModifySshKeysCommand cmd) {

View File

@ -31,6 +31,6 @@ public final class CitrixCheckOnHostCommandWrapper extends CommandWrapper<CheckO
@Override
public Answer execute(final CheckOnHostCommand command, final CitrixResourceBase citrixResourceBase) {
return new CheckOnHostAnswer(command, "Not Implmeneted");
return new CheckOnHostAnswer(command, "Not Implemented");
}
}

View File

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

View File

@ -30,6 +30,7 @@ import javax.annotation.Nonnull;
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.cloud.utils.storage.TemplateDownloaderUtil;
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 class LinstorStorageAdaptor implements StorageAdaptor {
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);
@ -763,7 +781,8 @@ public class LinstorStorageAdaptor implements StorageAdaptor {
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) {

View File

@ -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 class LinstorStoragePool implements KVMStoragePool {
return _resourceGroup;
}
public boolean isInsecureSsl() {
return _insecureSsl;
}
@Override
public boolean isPoolSupportHA() {
return true;

View File

@ -31,6 +31,7 @@ import com.linbit.linstor.api.model.ResourceMakeAvailable;
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;
@ -216,9 +217,15 @@ public class LinstorPrimaryDataStoreDriverImpl implements PrimaryDataStoreDriver
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
{
@ -256,7 +263,7 @@ public class LinstorPrimaryDataStoreDriverImpl implements PrimaryDataStoreDriver
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
{
@ -427,7 +434,7 @@ public class LinstorPrimaryDataStoreDriverImpl implements PrimaryDataStoreDriver
}
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(
@ -490,7 +497,7 @@ public class LinstorPrimaryDataStoreDriverImpl implements PrimaryDataStoreDriver
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();
@ -688,14 +695,14 @@ public class LinstorPrimaryDataStoreDriverImpl implements PrimaryDataStoreDriver
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);
@ -953,7 +960,7 @@ public class LinstorPrimaryDataStoreDriverImpl implements PrimaryDataStoreDriver
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(),
@ -1001,7 +1008,7 @@ public class LinstorPrimaryDataStoreDriverImpl implements PrimaryDataStoreDriver
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();
@ -1106,7 +1113,7 @@ public class LinstorPrimaryDataStoreDriverImpl implements PrimaryDataStoreDriver
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;
@ -1187,7 +1194,7 @@ public class LinstorPrimaryDataStoreDriverImpl implements PrimaryDataStoreDriver
{
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();
@ -1261,7 +1268,7 @@ public class LinstorPrimaryDataStoreDriverImpl implements PrimaryDataStoreDriver
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();
@ -1305,7 +1312,9 @@ public class LinstorPrimaryDataStoreDriverImpl implements PrimaryDataStoreDriver
@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
@ -1317,8 +1326,8 @@ public class LinstorPrimaryDataStoreDriverImpl implements PrimaryDataStoreDriver
* 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(
@ -1367,7 +1376,9 @@ public class LinstorPrimaryDataStoreDriverImpl implements PrimaryDataStoreDriver
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);

View File

@ -40,6 +40,7 @@ import com.cloud.storage.StorageManager;
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.PrimaryDataStoreLifeCy
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 class LinstorPrimaryDataStoreLifeCycleImpl extends BasePrimaryDataStoreLi
}
}
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 class LinstorPrimaryDataStoreLifeCycleImpl extends BasePrimaryDataStoreLi
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 class LinstorPrimaryDataStoreLifeCycleImpl extends BasePrimaryDataStoreLi
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) {

View File

@ -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<String, Object> 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<String, ConfigKey.Scope, Long> updated = (Ternary<String, ConfigKey.Scope, Long>) 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);
}
}
}
}
}

View File

@ -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<String> 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<Boolean> 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

View File

@ -51,6 +51,7 @@ import java.util.Optional;
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 class LinstorUtil {
);
}
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<StoragePool> storagePools = getRscGroupStoragePools(linstorApi, rscGroupName);
@ -239,8 +257,8 @@ public class LinstorUtil {
}
}
public static Pair<Long, Long> getStorageStats(String linstorUrl, String rscGroupName) {
DevelopersApi linstorApi = getLinstorAPI(linstorUrl);
public static Pair<Long, Long> getStorageStats(String linstorUrl, String rscGroupName, String apiToken, boolean insecureSsl) {
DevelopersApi linstorApi = getLinstorAPI(linstorUrl, apiToken, insecureSsl);
try {
List<StoragePool> storagePools = LinstorUtil.getRscGroupStoragePools(linstorApi, rscGroupName);
@ -576,7 +594,8 @@ public class LinstorUtil {
* @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<ResourceWithVolumes> resWithVols = api.viewResources(
Collections.emptyList(),
@ -831,7 +850,9 @@ public class LinstorUtil {
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();

View File

@ -70,6 +70,7 @@ import org.apache.cloudstack.storage.command.CopyCmdAnswer;
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 @@ public class LinstorDataMotionStrategy implements DataMotionStrategy {
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 @@ public class LinstorDataMotionStrategy implements DataMotionStrategy {
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 {

View File

@ -23,6 +23,7 @@ import com.linbit.linstor.api.DevelopersApi;
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 com.cloud.vm.snapshot.dao.VMSnapshotDao;
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 class LinstorVMSnapshotStrategy extends DefaultVMSnapshotStrategy {
try {
final List<VolumeObjectTO> 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 class LinstorVMSnapshotStrategy extends DefaultVMSnapshotStrategy {
List<VolumeObjectTO> 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<String> failedToDelete = new ArrayList<>();
@ -272,7 +280,7 @@ public class LinstorVMSnapshotStrategy extends DefaultVMSnapshotStrategy {
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 @@ public class LinstorVMSnapshotStrategy extends DefaultVMSnapshotStrategy {
List<VolumeObjectTO> 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) {

View File

@ -33,6 +33,8 @@
class="org.apache.cloudstack.storage.snapshot.LinstorVMSnapshotStrategy" />
<bean id="linstorConfigManager"
class="org.apache.cloudstack.storage.datastore.util.LinstorConfigurationManager" />
<bean id="linstorConfigChangeListener"
class="org.apache.cloudstack.storage.datastore.util.LinstorConfigChangeListener" />
<bean id="linstorDataMotionStrategy"
class="org.apache.cloudstack.storage.motion.LinstorDataMotionStrategy" />
</beans>

View File

@ -179,7 +179,7 @@
<cs.nitro.version>10.1</cs.nitro.version>
<cs.opensaml.version>2.6.6</cs.opensaml.version>
<cs.rados-java.version>0.6.0</cs.rados-java.version>
<cs.java-linstor.version>0.6.1</cs.java-linstor.version>
<cs.java-linstor.version>0.7.0</cs.java-linstor.version>
<cs.reflections.version>0.10.2</cs.reflections.version>
<cs.servicemix.version>3.4.4_1</cs.servicemix.version>
<cs.servlet.version>4.0.1</cs.servlet.version>

View File

@ -135,21 +135,16 @@ public class HostJoinDaoImpl extends GenericDaoBase<HostJoinVO, Long> implements
hostResponse.setHypervisor(hypervisorType);
}
hostResponse.setHostType(host.getType());
if (host.getType().equals(Host.Type.ConsoleProxy) || host.getType().equals(Host.Type.SecondaryStorageVM)) {
if (!details.contains(HostDetails.core) && (host.getType().equals(Host.Type.ConsoleProxy) || host.getType().equals(Host.Type.SecondaryStorageVM))) {
VMInstanceVO vm = virtualMachineDao.findVMByInstanceNameIncludingRemoved(host.getName());
if (vm != null) {
hostResponse.setVirtualMachineId(vm.getUuid());
}
}
hostResponse.setLastPinged(new Date(host.getLastPinged()));
Long mshostId = host.getManagementServerId();
if (mshostId != null) {
ManagementServerHostVO managementServer = managementServerHostDao.findByMsid(host.getManagementServerId());
if (managementServer != null) {
hostResponse.setManagementServerId(managementServer.getUuid());
hostResponse.setManagementServerName(managementServer.getName());
}
}
setManagementServerResponse(hostResponse, host, details);
hostResponse.setName(host.getName());
hostResponse.setPodId(host.getPodUuid());
hostResponse.setRemoved(host.getRemoved());
@ -159,41 +154,44 @@ public class HostJoinDaoImpl extends GenericDaoBase<HostJoinVO, Long> implements
hostResponse.setVersion(host.getVersion());
hostResponse.setCreated(host.getCreated());
List<HostGpuGroupsVO> gpuGroups = ApiDBUtils.getGpuGroups(host.getId());
if (gpuGroups != null && !gpuGroups.isEmpty()) {
List<GpuResponse> gpus = new ArrayList<GpuResponse>();
long gpuRemaining = 0;
long gpuTotal = 0;
for (HostGpuGroupsVO entry : gpuGroups) {
GpuResponse gpuResponse = new GpuResponse();
gpuResponse.setGpuGroupName(entry.getGroupName());
List<VGPUTypesVO> vgpuTypes = ApiDBUtils.getVgpus(entry.getId());
if (vgpuTypes != null && !vgpuTypes.isEmpty()) {
List<VgpuResponse> vgpus = new ArrayList<VgpuResponse>();
for (VGPUTypesVO vgpuType : vgpuTypes) {
VgpuResponse vgpuResponse = new VgpuResponse();
vgpuResponse.setName(vgpuType.getVgpuType());
vgpuResponse.setVideoRam(vgpuType.getVideoRam());
vgpuResponse.setMaxHeads(vgpuType.getMaxHeads());
vgpuResponse.setMaxResolutionX(vgpuType.getMaxResolutionX());
vgpuResponse.setMaxResolutionY(vgpuType.getMaxResolutionY());
vgpuResponse.setMaxVgpuPerPgpu(vgpuType.getMaxVgpuPerPgpu());
vgpuResponse.setRemainingCapacity(vgpuType.getRemainingCapacity());
vgpuResponse.setmaxCapacity(vgpuType.getMaxCapacity());
vgpus.add(vgpuResponse);
gpuRemaining += vgpuType.getRemainingCapacity();
gpuTotal += vgpuType.getMaxCapacity();
if (!details.contains(HostDetails.core)) {
List<HostGpuGroupsVO> gpuGroups = ApiDBUtils.getGpuGroups(host.getId());
if (gpuGroups != null && !gpuGroups.isEmpty()) {
List<GpuResponse> gpus = new ArrayList<GpuResponse>();
long gpuRemaining = 0;
long gpuTotal = 0;
for (HostGpuGroupsVO entry : gpuGroups) {
GpuResponse gpuResponse = new GpuResponse();
gpuResponse.setGpuGroupName(entry.getGroupName());
List<VGPUTypesVO> vgpuTypes = ApiDBUtils.getVgpus(entry.getId());
if (vgpuTypes != null && !vgpuTypes.isEmpty()) {
List<VgpuResponse> vgpus = new ArrayList<VgpuResponse>();
for (VGPUTypesVO vgpuType : vgpuTypes) {
VgpuResponse vgpuResponse = new VgpuResponse();
vgpuResponse.setName(vgpuType.getVgpuType());
vgpuResponse.setVideoRam(vgpuType.getVideoRam());
vgpuResponse.setMaxHeads(vgpuType.getMaxHeads());
vgpuResponse.setMaxResolutionX(vgpuType.getMaxResolutionX());
vgpuResponse.setMaxResolutionY(vgpuType.getMaxResolutionY());
vgpuResponse.setMaxVgpuPerPgpu(vgpuType.getMaxVgpuPerPgpu());
vgpuResponse.setRemainingCapacity(vgpuType.getRemainingCapacity());
vgpuResponse.setmaxCapacity(vgpuType.getMaxCapacity());
vgpus.add(vgpuResponse);
gpuRemaining += vgpuType.getRemainingCapacity();
gpuTotal += vgpuType.getMaxCapacity();
}
gpuResponse.setVgpu(vgpus);
}
gpuResponse.setVgpu(vgpus);
gpus.add(gpuResponse);
}
gpus.add(gpuResponse);
hostResponse.setGpuTotal(gpuTotal);
hostResponse.setGpuUsed(gpuTotal - gpuRemaining);
hostResponse.setGpuGroup(gpus);
}
hostResponse.setGpuTotal(gpuTotal);
hostResponse.setGpuUsed(gpuTotal - gpuRemaining);
hostResponse.setGpuGroup(gpus);
}
if (details.contains(HostDetails.all) || details.contains(HostDetails.capacity) || details.contains(HostDetails.stats) || details.contains(HostDetails.events)) {
if (details.contains(HostDetails.all) || details.contains(HostDetails.capacity) || details.contains(HostDetails.core) ||
details.contains(HostDetails.stats) || details.contains(HostDetails.events)) {
hostResponse.setOsCategoryId(host.getOsCategoryUuid());
hostResponse.setOsCategoryName(host.getOsCategoryName());
hostResponse.setZoneName(host.getZoneName());
@ -206,23 +204,9 @@ public class HostJoinDaoImpl extends GenericDaoBase<HostJoinVO, Long> implements
DecimalFormat decimalFormat = new DecimalFormat("#.##");
if (host.getType() == Host.Type.Routing) {
float cpuOverprovisioningFactor = ApiDBUtils.getCpuOverprovisioningFactor(host.getClusterId());
if (details.contains(HostDetails.all) || details.contains(HostDetails.capacity)) {
// set allocated capacities
Long mem = host.getMemReservedCapacity() + host.getMemUsedCapacity();
Long cpu = host.getCpuReservedCapacity() + host.getCpuUsedCapacity();
Float memWithOverprovisioning = host.getTotalMemory() * ApiDBUtils.getMemOverprovisioningFactor(host.getClusterId());
hostResponse.setMemoryTotal(memWithOverprovisioning.longValue());
hostResponse.setMemWithOverprovisioning(decimalFormat.format(memWithOverprovisioning));
hostResponse.setMemoryAllocated(mem);
hostResponse.setMemoryAllocatedBytes(mem);
hostResponse.setMemoryAllocatedPercentage(calculateResourceAllocatedPercentage(mem, memWithOverprovisioning));
if (details.contains(HostDetails.all) || details.contains(HostDetails.capacity) || details.contains(HostDetails.core)) {
String hostTags = host.getTag();
hostResponse.setHostTags(hostTags);
hostResponse.setIsTagARule(host.getIsTagARule());
hostResponse.setHaHost(containsHostHATag(hostTags));
hostResponse.setExplicitHostTags(host.getExplicitTag());
hostResponse.setImplicitHostTags(host.getImplicitTag());
hostResponse.setGuestOsRule(host.getGuestOsRule());
@ -236,14 +220,36 @@ public class HostJoinDaoImpl extends GenericDaoBase<HostJoinVO, Long> implements
hostResponse.setClusterStorageAccessGroups(host.getClusterStorageAccessGroups());
hostResponse.setPodStorageAccessGroups(host.getPodStorageAccessGroups());
hostResponse.setZoneStorageAccessGroups(host.getZoneStorageAccessGroups());
hostResponse.setHypervisorVersion(host.getHypervisorVersion());
float cpuWithOverprovisioning = host.getCpus() * host.getSpeed() * cpuOverprovisioningFactor;
hostResponse.setCpuAllocatedValue(cpu);
String cpuAllocated = calculateResourceAllocatedPercentage(cpu, cpuWithOverprovisioning);
hostResponse.setCpuAllocated(cpuAllocated);
hostResponse.setCpuAllocatedPercentage(cpuAllocated);
hostResponse.setCpuAllocatedWithOverprovisioning(cpuAllocated);
hostResponse.setCpuWithOverprovisioning(decimalFormat.format(cpuWithOverprovisioning));
if (!details.contains(HostDetails.core)) {
float cpuOverprovisioningFactor = ApiDBUtils.getCpuOverprovisioningFactor(host.getClusterId());
// set allocated capacities
Long mem = host.getMemReservedCapacity() + host.getMemUsedCapacity();
Long cpu = host.getCpuReservedCapacity() + host.getCpuUsedCapacity();
Float memWithOverprovisioning = host.getTotalMemory() * ApiDBUtils.getMemOverprovisioningFactor(host.getClusterId());
hostResponse.setMemoryTotal(memWithOverprovisioning.longValue());
hostResponse.setMemWithOverprovisioning(decimalFormat.format(memWithOverprovisioning));
hostResponse.setMemoryAllocated(mem);
hostResponse.setMemoryAllocatedBytes(mem);
hostResponse.setMemoryAllocatedPercentage(calculateResourceAllocatedPercentage(mem, memWithOverprovisioning));
hostResponse.setIsTagARule(host.getIsTagARule());
hostResponse.setHaHost(containsHostHATag(hostTags));
if (host.getArch() != null) {
hostResponse.setArch(host.getArch().getType());
}
float cpuWithOverprovisioning = host.getCpus() * host.getSpeed() * cpuOverprovisioningFactor;
hostResponse.setCpuAllocatedValue(cpu);
String cpuAllocated = calculateResourceAllocatedPercentage(cpu, cpuWithOverprovisioning);
hostResponse.setCpuAllocated(cpuAllocated);
hostResponse.setCpuAllocatedPercentage(cpuAllocated);
hostResponse.setCpuAllocatedWithOverprovisioning(cpuAllocated);
hostResponse.setCpuWithOverprovisioning(decimalFormat.format(cpuWithOverprovisioning));
}
}
if (details.contains(HostDetails.all) || details.contains(HostDetails.stats)) {
@ -262,27 +268,29 @@ public class HostJoinDaoImpl extends GenericDaoBase<HostJoinVO, Long> implements
}
}
Map<String, String> hostDetails = hostDetailsDao.findDetails(host.getId());
if (hostDetails != null) {
if (hostDetails.containsKey(Host.HOST_UEFI_ENABLE)) {
hostResponse.setUefiCapability(Boolean.parseBoolean((String) hostDetails.get(Host.HOST_UEFI_ENABLE)));
} else {
hostResponse.setUefiCapability(new Boolean(false));
if (!details.contains(HostDetails.core)) {
Map<String, String> hostDetails = hostDetailsDao.findDetails(host.getId());
if (hostDetails != null) {
if (hostDetails.containsKey(Host.HOST_UEFI_ENABLE)) {
hostResponse.setUefiCapability(Boolean.parseBoolean((String) hostDetails.get(Host.HOST_UEFI_ENABLE)));
} else {
hostResponse.setUefiCapability(new Boolean(false));
}
}
}
if (details.contains(HostDetails.all) &&
Arrays.asList(Hypervisor.HypervisorType.KVM,
Hypervisor.HypervisorType.Custom,
Hypervisor.HypervisorType.External).contains(host.getHypervisorType())) {
//only kvm has the requirement to return host details
try {
hostResponse.setDetails(hostDetails, host.getHypervisorType());
} catch (Exception e) {
logger.debug("failed to get host details", e);
if (details.contains(HostDetails.all) &&
Arrays.asList(Hypervisor.HypervisorType.KVM,
Hypervisor.HypervisorType.Custom,
Hypervisor.HypervisorType.External).contains(host.getHypervisorType())) {
//only kvm has the requirement to return host details
try {
hostResponse.setDetails(hostDetails, host.getHypervisorType());
} catch (Exception e) {
logger.debug("failed to get host details", e);
}
}
}
} else if (host.getType() == Host.Type.SecondaryStorage) {
} else if (host.getType() == Host.Type.SecondaryStorage && !details.contains(HostDetails.core)) {
StorageStats secStorageStats = ApiDBUtils.getSecondaryStorageStatistics(host.getId());
if (secStorageStats != null) {
hostResponse.setDiskSizeTotal(secStorageStats.getCapacityBytes());
@ -290,7 +298,16 @@ public class HostJoinDaoImpl extends GenericDaoBase<HostJoinVO, Long> implements
}
}
hostResponse.setLocalStorageActive(ApiDBUtils.isLocalStorageActiveOnHost(host.getId()));
if (!details.contains(HostDetails.core)) {
hostResponse.setLocalStorageActive(ApiDBUtils.isLocalStorageActiveOnHost(host.getId()));
hostResponse.setHostHAResponse(haConfigDao.findHAResource(host.getId(), HAResource.ResourceType.Host));
hostResponse.setOutOfBandManagementResponse(outOfBandManagementDao.findByHost(host.getId()));
hostResponse.setHasAnnotation(annotationDao.hasAnnotations(host.getUuid(), AnnotationService.EntityType.HOST.name(),
accountManager.isRootAdmin(CallContext.current().getCallingAccount().getId())));
hostResponse.setAnnotation(host.getAnnotation());
hostResponse.setLastAnnotated(host.getLastAnnotated());
hostResponse.setUsername(host.getUsername());
}
if (details.contains(HostDetails.all) || details.contains(HostDetails.events)) {
Set<com.cloud.host.Status.Event> possibleEvents = host.getStatus().getPossibleEvents();
@ -308,8 +325,6 @@ public class HostJoinDaoImpl extends GenericDaoBase<HostJoinVO, Long> implements
}
}
hostResponse.setHostHAResponse(haConfigDao.findHAResource(host.getId(), HAResource.ResourceType.Host));
hostResponse.setOutOfBandManagementResponse(outOfBandManagementDao.findByHost(host.getId()));
hostResponse.setResourceState(host.getResourceState().toString());
// set async job
@ -317,15 +332,25 @@ public class HostJoinDaoImpl extends GenericDaoBase<HostJoinVO, Long> implements
hostResponse.setJobId(host.getJobUuid());
hostResponse.setJobStatus(host.getJobStatus());
}
hostResponse.setHasAnnotation(annotationDao.hasAnnotations(host.getUuid(), AnnotationService.EntityType.HOST.name(),
accountManager.isRootAdmin(CallContext.current().getCallingAccount().getId())));
hostResponse.setAnnotation(host.getAnnotation());
hostResponse.setLastAnnotated(host.getLastAnnotated ());
hostResponse.setUsername(host.getUsername());
hostResponse.setObjectName("host");
}
private void setManagementServerResponse(HostResponse hostResponse, HostJoinVO host, EnumSet<HostDetails> details) {
if (host.getManagementServerId() != null) {
if (details.size() == 1 && details.contains(HostDetails.core)) {
// msid is returned as-is; callers resolve it to avoid a per-host lookup
hostResponse.setMsId(host.getManagementServerId());
} else {
ManagementServerHostVO managementServer = managementServerHostDao.findByMsid(host.getManagementServerId());
if (managementServer != null) {
hostResponse.setManagementServerId(managementServer.getUuid());
hostResponse.setManagementServerName(managementServer.getName());
}
}
}
}
@Override
public HostResponse newMinimalHostResponse(HostJoinVO host) {
HostResponse hostResponse = new HostResponse();

View File

@ -2396,14 +2396,16 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic
return a.equals(b);
}
private VolumeVO resizeVolumeInternal(VolumeVO volume, DiskOfferingVO newDiskOffering, Long currentSize, Long newSize, Long newMinIops, Long newMaxIops, Integer newHypervisorSnapshotReserve, boolean shrinkOk) throws ResourceAllocationException {
VolumeVO resizeVolumeInternal(VolumeVO volume, DiskOfferingVO newDiskOffering, Long currentSize, Long newSize, Long newMinIops, Long newMaxIops, Integer newHypervisorSnapshotReserve, boolean shrinkOk) throws ResourceAllocationException {
UserVmVO userVm = _userVmDao.findById(volume.getInstanceId());
HypervisorType hypervisorType = _volsDao.getHypervisorType(volume.getId());
if (userVm != null) {
if (volume.getVolumeType().equals(Volume.Type.ROOT) && userVm.getPowerState() != VirtualMachine.PowerState.PowerOff && hypervisorType == HypervisorType.VMware) {
logger.error(" For ROOT volume resize VM should be in Power Off state.");
throw new InvalidParameterValueException("VM current state is : " + userVm.getPowerState() + ". But VM should be in " + VirtualMachine.PowerState.PowerOff + " state.");
if (Volume.Type.ROOT.equals(volume.getVolumeType())
&& !State.Stopped.equals(userVm.getState())
&& HypervisorType.VMware.equals(hypervisorType)) {
logger.error("For ROOT volume resize VM should be in Stopped state.");
throw new InvalidParameterValueException("The current VM state is '" + userVm.getState() + "'. But the VM should be in " + State.Stopped + " state.");
}
// serialize VM operation
AsyncJobExecutionContext jobContext = AsyncJobExecutionContext.getCurrentExecutionContext();
@ -2457,7 +2459,7 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic
shrinkOk);
}
private void validateVolumeReadyStateAndHypervisorChecks(VolumeVO volume, long currentSize, Long newSize) {
void validateVolumeReadyStateAndHypervisorChecks(VolumeVO volume, long currentSize, Long newSize) {
// checking if there are any ongoing snapshots on the volume which is to be resized
List<SnapshotVO> ongoingSnapshots = _snapshotDao.listByStatus(volume.getId(), Snapshot.State.Creating, Snapshot.State.CreatedOnPrimary, Snapshot.State.BackingUp);
if (ongoingSnapshots.size() > 0) {
@ -2481,9 +2483,11 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic
UserVmVO userVm = _userVmDao.findById(volume.getInstanceId());
if (userVm != null) {
if (volume.getVolumeType().equals(Volume.Type.ROOT) && userVm.getPowerState() != VirtualMachine.PowerState.PowerOff && hypervisorType == HypervisorType.VMware) {
logger.error(" For ROOT volume resize VM should be in Power Off state.");
throw new InvalidParameterValueException("VM current state is : " + userVm.getPowerState() + ". But VM should be in " + VirtualMachine.PowerState.PowerOff + " state.");
if (Volume.Type.ROOT.equals(volume.getVolumeType())
&& !State.Stopped.equals(userVm.getState())
&& HypervisorType.VMware.equals(hypervisorType)) {
logger.error("For ROOT volume resize VM should be in Stopped state.");
throw new InvalidParameterValueException("The current VM state is '" + userVm.getState() + "'. But VM should be in " + State.Stopped + " state.");
}
}
}
@ -2500,7 +2504,7 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic
}
}
private void validateVolumeResizeWithNewDiskOfferingAndLoad(VolumeVO volume, DiskOfferingVO existingDiskOffering, DiskOfferingVO newDiskOffering, Long[] newSize, Long[] newMinIops, Long[] newMaxIops, Integer[] newHypervisorSnapshotReserve) {
void validateVolumeResizeWithNewDiskOfferingAndLoad(VolumeVO volume, DiskOfferingVO existingDiskOffering, DiskOfferingVO newDiskOffering, Long[] newSize, Long[] newMinIops, Long[] newMaxIops, Integer[] newHypervisorSnapshotReserve) {
if (newDiskOffering.getRemoved() != null) {
throw new InvalidParameterValueException("Requested disk offering has been removed.");
}

View File

@ -5397,11 +5397,31 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
return;
}
List<String> macAddresses = new ArrayList<>(vmNetworkStats.size());
for (VmNetworkStatsEntry entry : vmNetworkStats) {
macAddresses.add(entry.getMacAddress());
}
Map<String, NicVO> nicsByMac = new HashMap<>();
for (NicVO nic : _nicDao.listByMacAddresses(macAddresses)) {
nicsByMac.put(nic.getMacAddress(), nic);
}
Set<Long> networkIds = new HashSet<>();
for (NicVO nic : nicsByMac.values()) {
networkIds.add(nic.getNetworkId());
}
Map<Long, List<VlanVO>> vlansByNetwork = new HashMap<>();
for (VlanVO vlan : _vlanDao.listVlansByNetworkIds(new ArrayList<>(networkIds))) {
vlansByNetwork.computeIfAbsent(vlan.getNetworkId(), k -> new ArrayList<>()).add(vlan);
}
for (VmNetworkStatsEntry vmNetworkStat:vmNetworkStats) {
SearchCriteria<NicVO> sc_nic = _nicDao.createSearchCriteria();
sc_nic.addAnd("macAddress", SearchCriteria.Op.EQ, vmNetworkStat.getMacAddress());
NicVO nic = _nicDao.search(sc_nic, null).get(0);
List<VlanVO> vlan = _vlanDao.listVlansByNetworkId(nic.getNetworkId());
NicVO nic = nicsByMac.get(vmNetworkStat.getMacAddress());
if (nic == null) {
logger.warn("Unable to find nic for mac " + vmNetworkStat.getMacAddress());
continue;
}
List<VlanVO> vlan = vlansByNetwork.get(nic.getNetworkId());
if (vlan == null || vlan.size() == 0 || vlan.get(0).getVlanType() != VlanType.DirectAttached)
{
break; // only get network statistics for DirectAttached network (shared networks in Basic zone and Advanced zone with/without SG)

View File

@ -33,6 +33,7 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@ -40,16 +41,6 @@ import java.util.List;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import com.cloud.event.EventTypes;
import com.cloud.event.UsageEventUtils;
import com.cloud.host.HostVO;
import com.cloud.resourcelimit.CheckedReservation;
import com.cloud.service.ServiceOfferingVO;
import com.cloud.service.dao.ServiceOfferingDao;
import com.cloud.storage.clvm.ClvmPoolManager;
import com.cloud.vm.snapshot.VMSnapshot;
import com.cloud.vm.snapshot.VMSnapshotDetailsVO;
import com.cloud.vm.snapshot.dao.VMSnapshotDetailsDao;
import org.apache.cloudstack.acl.ControlledEntity;
import org.apache.cloudstack.acl.SecurityChecker.AccessType;
import org.apache.cloudstack.api.command.user.volume.CheckAndRepairVolumeCmd;
@ -71,6 +62,7 @@ import org.apache.cloudstack.engine.subsystem.api.storage.VolumeService.VolumeAp
import org.apache.cloudstack.framework.async.AsyncCallFuture;
import org.apache.cloudstack.framework.jobs.AsyncJobExecutionContext;
import org.apache.cloudstack.framework.jobs.AsyncJobManager;
import org.apache.cloudstack.framework.jobs.Outcome;
import org.apache.cloudstack.framework.jobs.dao.AsyncJobJoinMapDao;
import org.apache.cloudstack.framework.jobs.impl.AsyncJobVO;
import org.apache.cloudstack.resourcedetail.dao.SnapshotPolicyDetailsDao;
@ -109,20 +101,27 @@ import com.cloud.dc.HostPodVO;
import com.cloud.dc.dao.ClusterDao;
import com.cloud.dc.dao.DataCenterDao;
import com.cloud.dc.dao.HostPodDao;
import com.cloud.event.EventTypes;
import com.cloud.event.UsageEventUtils;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.exception.PermissionDeniedException;
import com.cloud.exception.ResourceAllocationException;
import com.cloud.host.HostVO;
import com.cloud.host.dao.HostDao;
import com.cloud.hypervisor.Hypervisor.HypervisorType;
import com.cloud.offering.DiskOffering;
import com.cloud.org.Grouping;
import com.cloud.projects.Project;
import com.cloud.projects.ProjectManager;
import com.cloud.resourcelimit.CheckedReservation;
import com.cloud.serializer.GsonHelper;
import com.cloud.server.ManagementService;
import com.cloud.server.TaggedResourceService;
import com.cloud.service.ServiceOfferingVO;
import com.cloud.service.dao.ServiceOfferingDao;
import com.cloud.storage.Storage.ProvisioningType;
import com.cloud.storage.Volume.Type;
import com.cloud.storage.clvm.ClvmPoolManager;
import com.cloud.storage.dao.DiskOfferingDao;
import com.cloud.storage.dao.SnapshotDao;
import com.cloud.storage.dao.StoragePoolTagsDao;
@ -148,8 +147,11 @@ import com.cloud.vm.VirtualMachine.State;
import com.cloud.vm.VirtualMachineManager;
import com.cloud.vm.dao.UserVmDao;
import com.cloud.vm.dao.VMInstanceDao;
import com.cloud.vm.snapshot.VMSnapshot;
import com.cloud.vm.snapshot.VMSnapshotDetailsVO;
import com.cloud.vm.snapshot.VMSnapshotVO;
import com.cloud.vm.snapshot.dao.VMSnapshotDao;
import com.cloud.vm.snapshot.dao.VMSnapshotDetailsDao;
@RunWith(MockitoJUnitRunner.class)
public class VolumeApiServiceImplTest {
@ -2712,4 +2714,186 @@ public class VolumeApiServiceImplTest {
throw new RuntimeException("Failed to invoke method: " + methodName, e);
}
}
// =====================================================================
// VMware ROOT-volume resize / offering-change: VM power_state-lag tests
//
// Both private guards that protect VMware ROOT-volume resize operations
// are covered here:
// validateVolumeReadyStateAndHypervisorChecks (called by changeDiskOfferingForVolumeInternal)
// resizeVolumeInternal (called by both resize and change-offering flows)
//
// The "power_state lag" scenario: when a VMware VM is stopped via CloudStack
// the VirtualMachine.State transitions to Stopped immediately, but the
// VMware-side VirtualMachine.PowerState (polled from ESX) may still read
// PoweredOn for some seconds. The production code must use only the
// authoritative CloudStack State field and must NOT additionally gate on
// PowerState.
// =====================================================================
/**
* Positive validateVolumeReadyStateAndHypervisorChecks:
* The guard must allow a VMware ROOT-volume resize when the CloudStack VM
* state is {@code Stopped}, regardless of the VMware power_state value.
* getPowerState() is intentionally left un-stubbed so that any invocation
* of that method would cause a Mockito strict-stubbing error and surface a
* regression.
*/
@Test
public void testValidateVolumeReadyStateVMware_VMStopped_PowerStateLag_ShouldPass()
throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
long volumeId = 200L;
long vmId = 201L;
VolumeVO volume = Mockito.mock(VolumeVO.class);
when(volume.getId()).thenReturn(volumeId);
when(volume.getInstanceId()).thenReturn(vmId);
when(volume.getVolumeType()).thenReturn(Volume.Type.ROOT);
when(volume.getState()).thenReturn(Volume.State.Ready);
// snapshotDaoMock returns an empty list by default (Mockito default behaviour)
when(volumeDaoMock.getHypervisorType(volumeId)).thenReturn(HypervisorType.VMware);
UserVmVO stoppedVm = Mockito.mock(UserVmVO.class);
// Authoritative cloud state: Stopped.
// getPowerState() is NOT stubbed power_state lag scenario.
when(stoppedVm.getState()).thenReturn(State.Stopped);
when(userVmDaoMock.findById(vmId)).thenReturn(stoppedVm);
long currentSizeBytes = 10L << 30; // 10 GiB
Long newSizeBytes = 20L << 30; // 20 GiB (grow; VMware prohibits shrink)
// Must complete without throwing any exception
volumeApiServiceImpl.validateVolumeReadyStateAndHypervisorChecks(volume, currentSizeBytes, newSizeBytes);
}
/**
* Negative validateVolumeReadyStateAndHypervisorChecks:
* The guard must reject a VMware ROOT-volume resize when the CloudStack VM
* state is {@code Running}.
*/
@Test(expected = InvalidParameterValueException.class)
public void testValidateVolumeReadyStateVMware_VMRunning_ShouldThrowInvalidParameterValueException()
throws NoSuchMethodException, IllegalAccessException {
long volumeId = 200L;
long vmId = 201L;
VolumeVO volume = Mockito.mock(VolumeVO.class);
when(volume.getId()).thenReturn(volumeId);
when(volume.getInstanceId()).thenReturn(vmId);
when(volume.getVolumeType()).thenReturn(Volume.Type.ROOT);
when(volume.getState()).thenReturn(Volume.State.Ready);
when(volumeDaoMock.getHypervisorType(volumeId)).thenReturn(HypervisorType.VMware);
UserVmVO runningVm = Mockito.mock(UserVmVO.class);
when(runningVm.getState()).thenReturn(State.Running);
when(userVmDaoMock.findById(vmId)).thenReturn(runningVm);
long currentSizeBytes = 10L << 30;
Long newSizeBytes = 20L << 30;
volumeApiServiceImpl.validateVolumeReadyStateAndHypervisorChecks(volume, currentSizeBytes, newSizeBytes);
Assert.fail("Expected InvalidParameterValueException for VMware ROOT-volume resize "
+ "when VM state is Running");
}
/**
* Positive resizeVolumeInternal:
* The VMware stopped-state guard inside {@code resizeVolumeInternal} must NOT
* fire when the CloudStack VM state is {@code Stopped}, even when the VMware
* power_state has not yet transitioned to PowerOff.
* Any exception originating from deeper plumbing (job queue, storage layer)
* is acceptable; only the state-guard exception is a failure.
*/
@Test
public void testResizeVolumeInternal_VMware_VMStopped_PowerStateLag_ShouldNotThrowStateGuardError()
throws NoSuchMethodException, IllegalAccessException {
long volumeId = 300L;
long vmId = 301L;
VolumeVO volume = Mockito.mock(VolumeVO.class);
when(volume.getId()).thenReturn(volumeId);
when(volume.getInstanceId()).thenReturn(vmId);
when(volume.getVolumeType()).thenReturn(Volume.Type.ROOT);
UserVmVO stoppedVm = Mockito.mock(UserVmVO.class);
when(stoppedVm.getState()).thenReturn(State.Stopped); // authoritative cloud state
// getPowerState() deliberately NOT stubbed power_state lag scenario
when(userVmDaoMock.findById(vmId)).thenReturn(stoppedVm);
when(volumeDaoMock.getHypervisorType(volumeId)).thenReturn(HypervisorType.VMware);
// Stub the job-queue call so OutcomeImpl.s_jobMgr (static, uninitialized in tests)
// is never reached. The Outcome mock returns null from get() and a bare AsyncJobVO
// from getJob(), which is enough for the unmarshallResultObject path to return null.
@SuppressWarnings("unchecked")
Outcome<Volume> outcomemock = Mockito.mock(Outcome.class);
AsyncJobVO stubJob = new AsyncJobVO();
when(outcomemock.getJob()).thenReturn(stubJob);
doReturn(outcomemock).when(volumeApiServiceImpl).resizeVolumeThroughJobQueue(
anyLong(), anyLong(), anyLong(), anyLong(),
nullable(Long.class), nullable(Long.class),
nullable(Integer.class), nullable(Long.class), anyBoolean());
// resizeVolumeInternal(VolumeVO, DiskOfferingVO, Long, Long, Long, Long, Integer, boolean)
try {
volumeApiServiceImpl.resizeVolumeInternal(
volume,
/* newDiskOffering */ (DiskOfferingVO) null,
/* currentSize */ 0L,
/* newSize */ 1L,
/* newMinIops */ (Long) null,
/* newMaxIops */ (Long) null,
/* snapshotReserve */ (Integer) null,
/* shrinkOk */ false);
} catch (ResourceAllocationException e) {
Assert.fail("Unexpected ResourceAllocationException");
} catch (InvalidParameterValueException e) {
Assert.fail("VMware ROOT-volume resize must be allowed when CloudStack VM state is "
+ "Stopped, even under a power_state lag. Unexpected exception: "
+ e.getMessage());
}
}
/**
* Negative resizeVolumeInternal:
* The VMware stopped-state guard inside {@code resizeVolumeInternal} must
* reject the operation when the CloudStack VM state is {@code Running}.
*/
@Test
public void testResizeVolumeInternal_VMware_VMRunning_ShouldThrowStateGuardError()
throws NoSuchMethodException, IllegalAccessException {
long volumeId = 300L;
long vmId = 301L;
VolumeVO volume = Mockito.mock(VolumeVO.class);
when(volume.getId()).thenReturn(volumeId);
when(volume.getInstanceId()).thenReturn(vmId);
when(volume.getVolumeType()).thenReturn(Volume.Type.ROOT);
UserVmVO runningVm = Mockito.mock(UserVmVO.class);
when(runningVm.getState()).thenReturn(State.Running);
when(userVmDaoMock.findById(vmId)).thenReturn(runningVm);
when(volumeDaoMock.getHypervisorType(volumeId)).thenReturn(HypervisorType.VMware);
try {
volumeApiServiceImpl.resizeVolumeInternal(
volume,
(DiskOfferingVO) null,
0L, 1L, (Long) null, (Long) null, (Integer) null, false);
Assert.fail("Expected an InvalidParameterValueException for VMware ROOT-volume resize when VM state is Running");
} catch (ResourceAllocationException e) {
Assert.fail("Cause must be InvalidParameterValueException, was: " + e.getClass());
} catch (InvalidParameterValueException e) {
Assert.assertTrue(
"Exception message must reference Stopped-state requirement, was: " + e.getMessage(),
e.getMessage() != null && e.getMessage().contains("VM should be in"));
}
}
}

View File

@ -47,13 +47,13 @@ class CsAddress(CsDataBag):
def get_guest_if_by_network_id(self):
guest_interface = None
lowest_network_id = 1000
lowest_network_id = None
for interface in self.get_interfaces():
if interface.is_guest() and interface.is_added():
if not self.config.is_vpc():
return interface
network_id = self.config.guestnetwork().get_network_id(interface.get_device())
if network_id and network_id < lowest_network_id:
if network_id and (lowest_network_id is None or network_id < lowest_network_id):
lowest_network_id = network_id
guest_interface = interface
return guest_interface

View File

@ -107,7 +107,7 @@ class CsVpcGuestNetwork(CsDataBag):
self.conf.append(" AdvSendAdvert on;")
self.conf.append(" MinRtrAdvInterval 5;")
self.conf.append(" MaxRtrAdvInterval 15;")
if entry['router_guest_ip6'] == entry['router_guest_ip6_gateway']:
if entry['router_guest_ip6'] == entry['router_guest_ip6_gateway'] or self.cl.is_redundant():
self.conf.append(" prefix %s" % full_addr)
self.conf.append(" {")
self.conf.append(" AdvOnLink on;")

View File

@ -16,6 +16,7 @@
# under the License.
import logging
import os
import random
import time
import socket
@ -283,6 +284,17 @@ class TestLinstorVolumes(cloudstackTestCase):
cls.testdata = TestData(first_host.ipaddress).testdata
# Registering a Linstor pool makes the management server read the resource-group capacity from
# the controller. If the controller enforces authentication, that call needs an API token,
# supplied as the 'lin.auth.apitoken' add-pool detail. Provide it via LINSTOR_API_TOKEN so it is
# never hard-coded; leave it unset for an unauthenticated controller.
api_token = os.environ.get("LINSTOR_API_TOKEN")
if api_token:
for storage_key in (TestData.primaryStorage,
TestData.primaryStorageSameInstance,
TestData.primaryStorageDistinctInstance):
cls.testdata[storage_key]["details"]["lin.auth.apitoken"] = api_token
# Get Resources from Cloud Infrastructure
cls.zone = get_zone(cls.apiClient, zone_id=cls.testdata[TestData.zoneId])
cls.cluster = list_clusters(cls.apiClient)[0]

View File

@ -2274,6 +2274,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",
@ -3788,6 +3790,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.",

View File

@ -93,7 +93,10 @@ router.beforeEach((to, from, next) => {
return
}
store.commit('SET_LOGIN_FLAG', true)
store.commit('SET_MS_ID', Cookies.get('managementserverid'))
const MS_ID = Cookies.get('managementserverid')
if (MS_ID) {
store.commit('SET_MS_ID', MS_ID)
}
}
// store already loaded
if (store.getters.passwordChangeRequired) {

View File

@ -438,6 +438,18 @@
</template>
<a-input v-model:value="form.resourcegroup" :placeholder="$t('message.linstor.resourcegroup.description')" />
</a-form-item>
<a-form-item name="linstorApiToken" ref="linstorApiToken">
<template #label>
<tooltip-label :title="$t('label.linstor.apitoken')" :tooltip="$t('message.linstor.apitoken.description')"/>
</template>
<a-input v-model:value="form.linstorApiToken" :placeholder="$t('message.linstor.apitoken.description')" />
</a-form-item>
<a-form-item name="linstorInsecureSsl" ref="linstorInsecureSsl">
<template #label>
<tooltip-label :title="$t('label.linstor.ssl.insecure')" :tooltip="$t('message.linstor.ssl.insecure.description')"/>
</template>
<a-switch v-model:checked="form.linstorInsecureSsl" />
</a-form-item>
</div>
<a-form-item name="selectedTags" ref="selectedTags">
<template #label>
@ -512,7 +524,8 @@ export default {
this.form = reactive({
scope: 'cluster',
hypervisor: this.hypervisors[0],
provider: 'DefaultPrimary'
provider: 'DefaultPrimary',
linstorInsecureSsl: true
})
this.rules = reactive({
zone: [{ required: true, message: this.$t('label.required') }],
@ -961,6 +974,10 @@ export default {
url = this.linstorURL(server)
values.managed = false
params['details[0].resourceGroup'] = values.resourcegroup
if (values.linstorApiToken && values.linstorApiToken.length > 0) {
params['details[0].lin.auth.apitoken'] = values.linstorApiToken
}
params['details[0].lin.ssl.insecure'] = values.linstorInsecureSsl ? 'true' : 'false'
if (values.capacityIops && values.capacityIops.length > 0) {
params.capacityIops = values.capacityIops.split(',').join('')
}

View File

@ -187,6 +187,11 @@ export default {
this.setRules(field)
const fieldExists = this.isDisplayInput(field)
if (!fieldExists) {
// A display-gated toggle that defaults on (checked: true) must be seeded while still
// hidden, otherwise it binds to an undefined value and shows off once it becomes visible.
if ((field.switch || field.checkbox) && field.checked) {
this.form[field.key] = true
}
return
}
if (field.key === 'agentUserName' && !this.getPrefilled(field)) {

View File

@ -561,6 +561,25 @@ export default {
primaryStorageProtocol: 'Linstor'
}
},
{
title: 'label.linstor.apitoken',
key: 'primaryStorageLinstorApiToken',
placeHolder: 'message.linstor.apitoken.description',
required: false,
display: {
primaryStorageProtocol: 'Linstor'
}
},
{
title: 'label.linstor.ssl.insecure',
key: 'primaryStorageLinstorInsecureSsl',
switch: true,
checked: true,
required: false,
display: {
primaryStorageProtocol: 'Linstor'
}
},
{
title: 'label.provider',
key: 'provider',
@ -568,7 +587,10 @@ export default {
value: 'DefaultPrimary',
select: true,
required: true,
options: this.primaryStorageProviders
options: this.primaryStorageProviders,
hidden: {
primaryStorageProtocol: 'Linstor'
}
},
{
title: 'label.ismanaged',

View File

@ -1562,6 +1562,10 @@ export default {
url = this.linstorURL(server)
params.provider = 'Linstor'
params['details[0].resourceGroup'] = this.prefillContent.primaryStorageLinstorResourceGroup
if (this.prefillContent.primaryStorageLinstorApiToken) {
params['details[0].lin.auth.apitoken'] = this.prefillContent.primaryStorageLinstorApiToken
}
params['details[0].lin.ssl.insecure'] = (this.prefillContent.primaryStorageLinstorInsecureSsl === false) ? 'false' : 'true'
} else if (protocol === 'vmfs' || protocol === 'datastorecluster') {
let path = this.prefillContent.primaryStorageVmfsDatacenter
if (path.substring(0, 1) !== '/') {

View File

@ -572,7 +572,7 @@ export default {
this.networkOfferings = []
this.selectedNetworkOffering = {}
getAPI('listNetworkOfferings', params).then(json => {
this.networkOfferings = json.listnetworkofferingsresponse.networkoffering
this.networkOfferings = json.listnetworkofferingsresponse.networkoffering || []
this.networkOfferings = this.networkOfferings.filter(offering => offering.fornsx === this.selectedZone.isnsxenabled)
if (!this.selectedZone.routedmodeenabled) {
this.networkOfferings = this.networkOfferings.filter(offering => offering.networkmode !== 'ROUTED')