Merge branch '4.22'

This commit is contained in:
Suresh Kumar Anaparti 2026-04-10 12:02:40 +05:30
commit 11538df710
No known key found for this signature in database
GPG Key ID: D7CEAE3A9E71D0AA
26 changed files with 600 additions and 166 deletions

View File

@ -37,7 +37,7 @@ public interface VolumeImportUnmanageService extends PluggableService, Configura
Arrays.asList(Hypervisor.HypervisorType.KVM, Hypervisor.HypervisorType.VMware);
List<Storage.StoragePoolType> SUPPORTED_STORAGE_POOL_TYPES_FOR_KVM = Arrays.asList(Storage.StoragePoolType.NetworkFilesystem,
Storage.StoragePoolType.Filesystem, Storage.StoragePoolType.RBD);
Storage.StoragePoolType.Filesystem, Storage.StoragePoolType.RBD, Storage.StoragePoolType.SharedMountPoint);
ConfigKey<Boolean> AllowImportVolumeWithBackingFile = new ConfigKey<>(Boolean.class,
"allow.import.volume.with.backing.file",

View File

@ -45,4 +45,9 @@ public interface HostTagsDao extends GenericDao<HostTagVO, Long> {
HostTagResponse newHostTagResponse(HostTagVO hostTag);
List<HostTagVO> searchByIds(Long... hostTagIds);
/**
* List all host tags defined on hosts within a cluster
*/
List<String> listByClusterId(Long clusterId);
}

View File

@ -23,6 +23,7 @@ import org.apache.cloudstack.api.response.HostTagResponse;
import org.apache.cloudstack.framework.config.ConfigKey;
import org.apache.cloudstack.framework.config.Configurable;
import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
@ -43,9 +44,12 @@ public class HostTagsDaoImpl extends GenericDaoBase<HostTagVO, Long> implements
private final SearchBuilder<HostTagVO> stSearch;
private final SearchBuilder<HostTagVO> tagIdsearch;
private final SearchBuilder<HostTagVO> ImplicitTagsSearch;
private final GenericSearchBuilder<HostTagVO, String> tagSearch;
@Inject
private ConfigurationDao _configDao;
@Inject
private HostDao hostDao;
public HostTagsDaoImpl() {
HostSearch = createSearchBuilder();
@ -72,6 +76,11 @@ public class HostTagsDaoImpl extends GenericDaoBase<HostTagVO, Long> implements
ImplicitTagsSearch.and("hostId", ImplicitTagsSearch.entity().getHostId(), SearchCriteria.Op.EQ);
ImplicitTagsSearch.and("isImplicit", ImplicitTagsSearch.entity().getIsImplicit(), SearchCriteria.Op.EQ);
ImplicitTagsSearch.done();
tagSearch = createSearchBuilder(String.class);
tagSearch.selectFields(tagSearch.entity().getTag());
tagSearch.and("hostIdIN", tagSearch.entity().getHostId(), SearchCriteria.Op.IN);
tagSearch.done();
}
@Override
@ -235,4 +244,15 @@ public class HostTagsDaoImpl extends GenericDaoBase<HostTagVO, Long> implements
return tagList;
}
@Override
public List<String> listByClusterId(Long clusterId) {
List<Long> hostIds = hostDao.listIdsByClusterId(clusterId);
if (CollectionUtils.isEmpty(hostIds)) {
return new ArrayList<>();
}
SearchCriteria<String> sc = tagSearch.create();
sc.setParameters("hostIdIN", hostIds.toArray());
return customSearch(sc, null);
}
}

View File

@ -170,6 +170,7 @@ public class SnapshotDaoImpl extends GenericDaoBase<SnapshotVO, Long> implements
CountSnapshotsByAccount.select(null, Func.COUNT, null);
CountSnapshotsByAccount.and("account", CountSnapshotsByAccount.entity().getAccountId(), SearchCriteria.Op.EQ);
CountSnapshotsByAccount.and("status", CountSnapshotsByAccount.entity().getState(), SearchCriteria.Op.NIN);
CountSnapshotsByAccount.and("snapshotTypeNEQ", CountSnapshotsByAccount.entity().getSnapshotType(), SearchCriteria.Op.NIN);
CountSnapshotsByAccount.and("removed", CountSnapshotsByAccount.entity().getRemoved(), SearchCriteria.Op.NULL);
CountSnapshotsByAccount.done();
@ -220,6 +221,7 @@ public class SnapshotDaoImpl extends GenericDaoBase<SnapshotVO, Long> implements
SearchCriteria<Long> sc = CountSnapshotsByAccount.create();
sc.setParameters("account", accountId);
sc.setParameters("status", State.Error, State.Destroyed);
sc.setParameters("snapshotTypeNEQ", Snapshot.Type.GROUP.ordinal());
return customSearch(sc, null).get(0);
}

View File

@ -51,6 +51,8 @@ StateDao<ObjectInDataStoreStateMachine.State, ObjectInDataStoreStateMachine.Even
SnapshotDataStoreVO findBySnapshotIdAndDataStoreRoleAndState(long snapshotId, DataStoreRole role, ObjectInDataStoreStateMachine.State state);
List<SnapshotDataStoreVO> listBySnapshotIdAndDataStoreRoleAndStateIn(long snapshotId, DataStoreRole role, ObjectInDataStoreStateMachine.State... state);
List<SnapshotDataStoreVO> listReadyByVolumeIdAndCheckpointPathNotNull(long volumeId);
SnapshotDataStoreVO findOneBySnapshotId(long snapshotId, long zoneId);

View File

@ -68,6 +68,7 @@ public class SnapshotDataStoreDaoImpl extends GenericDaoBase<SnapshotDataStoreVO
protected SearchBuilder<SnapshotDataStoreVO> searchFilteringStoreIdEqStateEqStoreRoleEqIdEqUpdateCountEqSnapshotIdEqVolumeIdEq;
private SearchBuilder<SnapshotDataStoreVO> stateSearch;
private SearchBuilder<SnapshotDataStoreVO> idStateNinSearch;
private SearchBuilder<SnapshotDataStoreVO> idEqRoleEqStateInSearch;
protected SearchBuilder<SnapshotVO> snapshotVOSearch;
private SearchBuilder<SnapshotDataStoreVO> snapshotCreatedSearch;
private SearchBuilder<SnapshotDataStoreVO> dataStoreAndInstallPathSearch;
@ -151,6 +152,11 @@ public class SnapshotDataStoreDaoImpl extends GenericDaoBase<SnapshotDataStoreVO
idStateNinSearch.and(STATE, idStateNinSearch.entity().getState(), SearchCriteria.Op.NOTIN);
idStateNinSearch.done();
idEqRoleEqStateInSearch = createSearchBuilder();
idEqRoleEqStateInSearch.and(SNAPSHOT_ID, idEqRoleEqStateInSearch.entity().getSnapshotId(), SearchCriteria.Op.EQ);
idEqRoleEqStateInSearch.and(STORE_ROLE, idEqRoleEqStateInSearch.entity().getRole(), SearchCriteria.Op.EQ);
idEqRoleEqStateInSearch.and(STATE, idEqRoleEqStateInSearch.entity().getState(), SearchCriteria.Op.IN);
snapshotVOSearch = snapshotDao.createSearchBuilder();
snapshotVOSearch.and(VOLUME_ID, snapshotVOSearch.entity().getVolumeId(), SearchCriteria.Op.EQ);
snapshotVOSearch.done();
@ -387,6 +393,15 @@ public class SnapshotDataStoreDaoImpl extends GenericDaoBase<SnapshotDataStoreVO
return findOneBy(sc);
}
@Override
public List<SnapshotDataStoreVO> listBySnapshotIdAndDataStoreRoleAndStateIn(long snapshotId, DataStoreRole role, State... state) {
SearchCriteria<SnapshotDataStoreVO> sc = idEqRoleEqStateInSearch.create();
sc.setParameters(SNAPSHOT_ID, snapshotId);
sc.setParameters(STORE_ROLE, role);
sc.setParameters(STATE, (Object[])state);
return listBy(sc);
}
@Override
public SnapshotDataStoreVO findOneBySnapshotId(long snapshotId, long zoneId) {
try (TransactionLegacy transactionLegacy = TransactionLegacy.currentTxn()) {

View File

@ -687,22 +687,23 @@ CREATE TABLE IF NOT EXISTS `cloud`.`backup_details` (
UPDATE `cloud`.`backups` b
INNER JOIN `cloud`.`vm_instance` vm ON b.vm_id = vm.id
SET b.backed_volumes = (
SELECT CONCAT("[",
GROUP_CONCAT(
CONCAT(
"{\"uuid\":\"", v.uuid, "\",",
"\"type\":\"", v.volume_type, "\",",
"\"size\":", v.`size`, ",",
"\"path\":\"", IFNULL(v.path, 'null'), "\",",
"\"deviceId\":", IFNULL(v.device_id, 'null'), ",",
"\"diskOfferingId\":\"", doff.uuid, "\",",
"\"minIops\":", IFNULL(v.min_iops, 'null'), ",",
"\"maxIops\":", IFNULL(v.max_iops, 'null'),
"}"
)
SEPARATOR ","
SELECT COALESCE(
CAST(
JSON_ARRAYAGG(
JSON_OBJECT(
'uuid', v.uuid,
'type', v.volume_type,
'size', v.size,
'path', v.path,
'deviceId', v.device_id,
'diskOfferingId', doff.uuid,
'minIops', v.min_iops,
'maxIops', v.max_iops
)
) AS CHAR
),
"]")
'[]'
)
FROM `cloud`.`volumes` v
LEFT JOIN `cloud`.`disk_offering` doff ON v.disk_offering_id = doff.id
WHERE v.instance_id = vm.id
@ -711,22 +712,23 @@ SET b.backed_volumes = (
-- Add diskOfferingId, deviceId, minIops and maxIops to backup_volumes in vm_instance table
UPDATE `cloud`.`vm_instance` vm
SET vm.backup_volumes = (
SELECT CONCAT("[",
GROUP_CONCAT(
CONCAT(
"{\"uuid\":\"", v.uuid, "\",",
"\"type\":\"", v.volume_type, "\",",
"\"size\":", v.`size`, ",",
"\"path\":\"", IFNULL(v.path, 'null'), "\",",
"\"deviceId\":", IFNULL(v.device_id, 'null'), ",",
"\"diskOfferingId\":\"", doff.uuid, "\",",
"\"minIops\":", IFNULL(v.min_iops, 'null'), ",",
"\"maxIops\":", IFNULL(v.max_iops, 'null'),
"}"
)
SEPARATOR ","
SELECT COALESCE(
CAST(
JSON_ARRAYAGG(
JSON_OBJECT(
'uuid', v.uuid,
'type', v.volume_type,
'size', v.size,
'path', v.path,
'deviceId', v.device_id,
'diskOfferingId', doff.uuid,
'minIops', v.min_iops,
'maxIops', v.max_iops
)
) AS CHAR
),
"]")
'[]'
)
FROM `cloud`.`volumes` v
LEFT JOIN `cloud`.`disk_offering` doff ON v.disk_offering_id = doff.id
WHERE v.instance_id = vm.id

View File

@ -120,7 +120,7 @@ public class DefaultSnapshotStrategy extends SnapshotStrategyBase {
private final List<Snapshot.State> snapshotStatesAbleToDeleteSnapshot = Arrays.asList(Snapshot.State.Destroying, Snapshot.State.Destroyed, Snapshot.State.Error, Snapshot.State.Hidden);
public SnapshotDataStoreVO getSnapshotImageStoreRef(long snapshotId, long zoneId) {
List<SnapshotDataStoreVO> snaps = snapshotStoreDao.listReadyBySnapshot(snapshotId, DataStoreRole.Image);
List<SnapshotDataStoreVO> snaps = snapshotStoreDao.listBySnapshotIdAndDataStoreRoleAndStateIn(snapshotId, DataStoreRole.Image, State.Ready, State.Hidden);
for (SnapshotDataStoreVO ref : snaps) {
if (zoneId == dataStoreMgr.getStoreZoneId(ref.getDataStoreId(), ref.getRole())) {
return ref;

View File

@ -257,11 +257,6 @@ public class DefaultSnapshotStrategyTest {
@Test
public void testGetSnapshotImageStoreRefNull() {
SnapshotDataStoreVO ref1 = Mockito.mock(SnapshotDataStoreVO.class);
Mockito.when(ref1.getDataStoreId()).thenReturn(1L);
Mockito.when(ref1.getRole()).thenReturn(DataStoreRole.Image);
Mockito.when(snapshotDataStoreDao.listReadyBySnapshot(Mockito.anyLong(), Mockito.any(DataStoreRole.class))).thenReturn(List.of(ref1));
Mockito.when(dataStoreManager.getStoreZoneId(1L, DataStoreRole.Image)).thenReturn(2L);
Assert.assertNull(defaultSnapshotStrategySpy.getSnapshotImageStoreRef(1L, 1L));
}
@ -270,7 +265,7 @@ public class DefaultSnapshotStrategyTest {
SnapshotDataStoreVO ref1 = Mockito.mock(SnapshotDataStoreVO.class);
Mockito.when(ref1.getDataStoreId()).thenReturn(1L);
Mockito.when(ref1.getRole()).thenReturn(DataStoreRole.Image);
Mockito.when(snapshotDataStoreDao.listReadyBySnapshot(Mockito.anyLong(), Mockito.any(DataStoreRole.class))).thenReturn(List.of(ref1));
Mockito.when(snapshotDataStoreDao.listBySnapshotIdAndDataStoreRoleAndStateIn(Mockito.anyLong(), Mockito.any(DataStoreRole.class), Mockito.any(), Mockito.any())).thenReturn(List.of(ref1));
Mockito.when(dataStoreManager.getStoreZoneId(1L, DataStoreRole.Image)).thenReturn(1L);
Assert.assertNotNull(defaultSnapshotStrategySpy.getSnapshotImageStoreRef(1L, 1L));
}

View File

@ -79,28 +79,47 @@ public class LibvirtGpuDef {
gpuBuilder.append(" <driver name='vfio'/>\n");
gpuBuilder.append(" <source>\n");
// Parse the bus address (e.g., 00:02.0) into domain, bus, slot, function
String domain = "0x0000";
String bus = "0x00";
String slot = "0x00";
String function = "0x0";
// Parse the bus address into domain, bus, slot, function. Two input formats are accepted:
// - "dddd:bb:ss.f" full PCI address with domain (e.g. 0000:00:02.0)
// - "bb:ss.f" legacy short BDF; domain defaults to 0000
// Each segment is parsed as a hex integer and formatted with fixed widths
// (domain: 4 hex digits, bus/slot: 2 hex digits, function: 1 hex digit) to
// produce canonical libvirt XML values regardless of input casing or padding.
int domainVal = 0, busVal = 0, slotVal = 0, funcVal = 0;
if (busAddress != null && !busAddress.isEmpty()) {
String[] parts = busAddress.split(":");
if (parts.length > 1) {
bus = "0x" + parts[0];
String[] slotFunctionParts = parts[1].split("\\.");
if (slotFunctionParts.length > 0) {
slot = "0x" + slotFunctionParts[0];
if (slotFunctionParts.length > 1) {
function = "0x" + slotFunctionParts[1].trim();
}
try {
String slotFunction;
if (parts.length == 3) {
domainVal = Integer.parseInt(parts[0], 16);
busVal = Integer.parseInt(parts[1], 16);
slotFunction = parts[2];
} else if (parts.length == 2) {
busVal = Integer.parseInt(parts[0], 16);
slotFunction = parts[1];
} else {
throw new IllegalArgumentException("Invalid PCI bus address format: '" + busAddress + "'");
}
String[] sf = slotFunction.split("\\.");
if (sf.length == 2) {
slotVal = Integer.parseInt(sf[0], 16);
funcVal = Integer.parseInt(sf[1].trim(), 16);
} else {
throw new IllegalArgumentException("Invalid PCI bus address format: '" + busAddress + "'");
}
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Invalid PCI bus address format: '" + busAddress + "'", e);
}
}
String domain = String.format("0x%04x", domainVal);
String bus = String.format("0x%02x", busVal);
String slot = String.format("0x%02x", slotVal);
String function = String.format("0x%x", funcVal);
gpuBuilder.append(" <address domain='").append(domain).append("' bus='").append(bus).append("' slot='")
.append(slot).append("' function='").append(function.trim()).append("'/>\n");
.append(slot).append("' function='").append(function).append("'/>\n");
gpuBuilder.append(" </source>\n");
gpuBuilder.append("</hostdev>\n");
}

View File

@ -47,7 +47,10 @@ import java.util.Map;
@ResourceWrapper(handles = CheckVolumeCommand.class)
public final class LibvirtCheckVolumeCommandWrapper extends CommandWrapper<CheckVolumeCommand, Answer, LibvirtComputingResource> {
private static final List<Storage.StoragePoolType> STORAGE_POOL_TYPES_SUPPORTED = Arrays.asList(Storage.StoragePoolType.Filesystem, Storage.StoragePoolType.NetworkFilesystem);
private static final List<Storage.StoragePoolType> STORAGE_POOL_TYPES_SUPPORTED = Arrays.asList(
Storage.StoragePoolType.Filesystem,
Storage.StoragePoolType.NetworkFilesystem,
Storage.StoragePoolType.SharedMountPoint);
@Override
public Answer execute(final CheckVolumeCommand command, final LibvirtComputingResource libvirtComputingResource) {

View File

@ -52,7 +52,7 @@ import java.util.stream.Collectors;
public final class LibvirtGetVolumesOnStorageCommandWrapper extends CommandWrapper<GetVolumesOnStorageCommand, Answer, LibvirtComputingResource> {
static final List<StoragePoolType> STORAGE_POOL_TYPES_SUPPORTED_BY_QEMU_IMG = Arrays.asList(StoragePoolType.NetworkFilesystem,
StoragePoolType.Filesystem, StoragePoolType.RBD);
StoragePoolType.Filesystem, StoragePoolType.RBD, StoragePoolType.SharedMountPoint);
@Override
public Answer execute(final GetVolumesOnStorageCommand command, final LibvirtComputingResource libvirtComputingResource) {

View File

@ -88,7 +88,7 @@ public class LibvirtRestoreBackupCommandWrapper extends CommandWrapper<RestoreBa
List<PrimaryDataStoreTO> restoreVolumePools = command.getRestoreVolumePools();
List<String> restoreVolumePaths = command.getRestoreVolumePaths();
Integer mountTimeout = command.getMountTimeout() * 1000;
int timeout = command.getWait();
int timeout = command.getWait() > 0 ? command.getWait() * 1000 : serverResource.getCmdsTimeout();
KVMStoragePoolManager storagePoolMgr = serverResource.getStoragePoolMgr();
List<String> backupFiles = command.getBackupFiles();
@ -270,7 +270,7 @@ public class LibvirtRestoreBackupCommandWrapper extends CommandWrapper<RestoreBa
return replaceBlockDeviceWithBackup(storagePoolMgr, volumePool, volumePath, backupPath, timeout, createTargetVolume, size);
}
int exitValue = Script.runSimpleBashScriptForExitValue(String.format(RSYNC_COMMAND, backupPath, volumePath));
int exitValue = Script.runSimpleBashScriptForExitValue(String.format(RSYNC_COMMAND, backupPath, volumePath), timeout, false);
return exitValue == 0;
}
@ -278,7 +278,7 @@ public class LibvirtRestoreBackupCommandWrapper extends CommandWrapper<RestoreBa
KVMStoragePool volumeStoragePool = storagePoolMgr.getStoragePool(volumePool.getPoolType(), volumePool.getUuid());
QemuImg qemu;
try {
qemu = new QemuImg(timeout * 1000, true, false);
qemu = new QemuImg(timeout, true, false);
String volumeUuid = getVolumeUuidFromPath(volumePath, volumePool);
KVMPhysicalDisk disk = null;
if (createTargetVolume) {

View File

@ -52,6 +52,7 @@ public class LibvirtTakeBackupCommandWrapper extends CommandWrapper<TakeBackupCo
List<PrimaryDataStoreTO> volumePools = command.getVolumePools();
final List<String> volumePaths = command.getVolumePaths();
KVMStoragePoolManager storagePoolMgr = libvirtComputingResource.getStoragePoolMgr();
int timeout = command.getWait() > 0 ? command.getWait() * 1000 : libvirtComputingResource.getCmdsTimeout();
List<String> diskPaths = new ArrayList<>();
if (Objects.nonNull(volumePaths)) {
@ -81,7 +82,7 @@ public class LibvirtTakeBackupCommandWrapper extends CommandWrapper<TakeBackupCo
"-d", diskPaths.isEmpty() ? "" : String.join(",", diskPaths)
});
Pair<Integer, String> result = Script.executePipedCommands(commands, libvirtComputingResource.getCmdsTimeout());
Pair<Integer, String> result = Script.executePipedCommands(commands, timeout);
if (result.first() != 0) {
logger.debug("Failed to take VM backup: " + result.second());

View File

@ -115,6 +115,145 @@ public class LibvirtGpuDefTest extends TestCase {
assertTrue(gpuXml.contains("</hostdev>"));
}
@Test
public void testGpuDef_withFullPciAddressDomainZero() {
LibvirtGpuDef gpuDef = new LibvirtGpuDef();
VgpuTypesInfo pciGpuInfo = new VgpuTypesInfo(
GpuDevice.DeviceType.PCI,
"passthrough",
"passthrough",
"0000:00:02.0",
"10de",
"NVIDIA Corporation",
"1b38",
"Tesla T4"
);
gpuDef.defGpu(pciGpuInfo);
String gpuXml = gpuDef.toString();
assertTrue(gpuXml.contains("<address domain='0x0000' bus='0x00' slot='0x02' function='0x0'/>"));
}
@Test
public void testGpuDef_withFullPciAddressNonZeroDomain() {
LibvirtGpuDef gpuDef = new LibvirtGpuDef();
VgpuTypesInfo pciGpuInfo = new VgpuTypesInfo(
GpuDevice.DeviceType.PCI,
"passthrough",
"passthrough",
"0001:65:00.0",
"10de",
"NVIDIA Corporation",
"1b38",
"Tesla T4"
);
gpuDef.defGpu(pciGpuInfo);
String gpuXml = gpuDef.toString();
assertTrue(gpuXml.contains("<address domain='0x0001' bus='0x65' slot='0x00' function='0x0'/>"));
}
@Test
public void testGpuDef_withNvidiaStyleEightDigitDomain() {
// nvidia-smi reports PCI addresses with an 8-digit domain (e.g. "00000001:af:00.1").
// generatePciXml must normalize it to the canonical 4-digit form "0x0001".
LibvirtGpuDef gpuDef = new LibvirtGpuDef();
VgpuTypesInfo pciGpuInfo = new VgpuTypesInfo(
GpuDevice.DeviceType.PCI,
"passthrough",
"passthrough",
"00000001:af:00.1",
"10de",
"NVIDIA Corporation",
"1b38",
"Tesla T4"
);
gpuDef.defGpu(pciGpuInfo);
String gpuXml = gpuDef.toString();
assertTrue(gpuXml.contains("<address domain='0x0001' bus='0xaf' slot='0x00' function='0x1'/>"));
}
@Test
public void testGpuDef_withFullPciAddressVfNonZeroDomain() {
LibvirtGpuDef gpuDef = new LibvirtGpuDef();
VgpuTypesInfo vfGpuInfo = new VgpuTypesInfo(
GpuDevice.DeviceType.PCI,
"VF-Profile",
"VF-Profile",
"0002:81:00.3",
"10de",
"NVIDIA Corporation",
"1eb8",
"Tesla T4"
);
gpuDef.defGpu(vfGpuInfo);
String gpuXml = gpuDef.toString();
// Non-passthrough NVIDIA VFs should be unmanaged
assertTrue(gpuXml.contains("<hostdev mode='subsystem' type='pci' managed='no' display='off'>"));
assertTrue(gpuXml.contains("<address domain='0x0002' bus='0x81' slot='0x00' function='0x3'/>"));
}
@Test
public void testGpuDef_withLegacyShortBdfDefaultsDomainToZero() {
// Backward compatibility: short BDF with no domain segment must still
// produce a valid libvirt address with domain 0x0000.
LibvirtGpuDef gpuDef = new LibvirtGpuDef();
VgpuTypesInfo pciGpuInfo = new VgpuTypesInfo(
GpuDevice.DeviceType.PCI,
"passthrough",
"passthrough",
"af:00.0",
"10de",
"NVIDIA Corporation",
"1b38",
"Tesla T4"
);
gpuDef.defGpu(pciGpuInfo);
String gpuXml = gpuDef.toString();
assertTrue(gpuXml.contains("<address domain='0x0000' bus='0xaf' slot='0x00' function='0x0'/>"));
}
@Test
public void testGpuDef_withInvalidBusAddressThrows() {
String[] invalidAddresses = {
"notahex:00.0", // non-hex bus
"gg:00:02.0", // non-hex domain
"00:02:03:04", // too many colon-separated parts
"00", // missing slot/function
"00:02", // missing function (no dot)
"00:02.0.1", // extra dot in ss.f
};
for (String addr : invalidAddresses) {
LibvirtGpuDef gpuDef = new LibvirtGpuDef();
VgpuTypesInfo info = new VgpuTypesInfo(
GpuDevice.DeviceType.PCI,
"passthrough",
"passthrough",
addr,
"10de",
"NVIDIA Corporation",
"1b38",
"Tesla T4"
);
gpuDef.defGpu(info);
try {
String ignored = gpuDef.toString();
fail("Expected IllegalArgumentException for address: " + addr + " but got: " + ignored);
} catch (IllegalArgumentException e) {
assertTrue("Exception message should contain the bad address",
e.getMessage().contains(addr));
}
}
}
@Test
public void testGpuDef_withNullDeviceType() {
LibvirtGpuDef gpuDef = new LibvirtGpuDef();

View File

@ -391,7 +391,15 @@ public class LibvirtRestoreBackupCommandWrapperTest {
try (MockedStatic<Script> scriptMock = mockStatic(Script.class)) {
scriptMock.when(() -> Script.runSimpleBashScriptForExitValue(anyString(), anyInt(), any(Boolean.class)))
.thenReturn(0); // Mount success
.thenAnswer(invocation -> {
String command = invocation.getArgument(0);
if (command.contains("mount")) {
return 0; // mount success
} else if (command.contains("rsync")) {
return 1; // Rsync failure
}
return 0; // Other commands success
});
scriptMock.when(() -> Script.runSimpleBashScriptForExitValue(anyString()))
.thenAnswer(invocation -> {
String command = invocation.getArgument(0);
@ -399,8 +407,6 @@ public class LibvirtRestoreBackupCommandWrapperTest {
return 0; // File exists
} else if (command.contains("qemu-img check")) {
return 0; // File is valid
} else if (command.contains("rsync")) {
return 1; // Rsync failure
}
return 0; // Other commands success
});
@ -444,7 +450,15 @@ public class LibvirtRestoreBackupCommandWrapperTest {
try (MockedStatic<Script> scriptMock = mockStatic(Script.class)) {
scriptMock.when(() -> Script.runSimpleBashScriptForExitValue(anyString(), anyInt(), any(Boolean.class)))
.thenReturn(0); // Mount success
.thenAnswer(invocation -> {
String command = invocation.getArgument(0);
if (command.contains("mount")) {
return 0; // Mount success
} else if (command.contains("rsync")) {
return 0; // Rsync success
}
return 0; // Other commands success
});
scriptMock.when(() -> Script.runSimpleBashScriptForExitValue(anyString()))
.thenAnswer(invocation -> {
String command = invocation.getArgument(0);
@ -452,8 +466,6 @@ public class LibvirtRestoreBackupCommandWrapperTest {
return 0; // File exists
} else if (command.contains("qemu-img check")) {
return 0; // File is valid
} else if (command.contains("rsync")) {
return 0; // Rsync success
} else if (command.contains("virsh attach-disk")) {
return 1; // Attach failure
}
@ -539,10 +551,10 @@ public class LibvirtRestoreBackupCommandWrapperTest {
filesMock.when(() -> Files.createTempDirectory(anyString())).thenReturn(tempPath);
try (MockedStatic<Script> scriptMock = mockStatic(Script.class)) {
scriptMock.when(() -> Script.runSimpleBashScriptForExitValue(anyString(), anyInt(), any(Boolean.class)))
.thenReturn(0); // Mount success
scriptMock.when(() -> Script.runSimpleBashScriptForExitValue(anyString()))
.thenReturn(0); // All other commands success
.thenReturn(0); // All commands success
scriptMock.when(() -> Script.runSimpleBashScriptForExitValue(anyString(), anyInt(), any(Boolean.class)))
.thenReturn(0); // All commands success
filesMock.when(() -> Files.deleteIfExists(any(Path.class))).thenReturn(true);

View File

@ -357,7 +357,33 @@
# "vgpu_instances":[],
# "vf_instances":[]
# }
# },
# {
# "pci_address": "0001:65:00.0",
# "vendor_id": "10de",
# "device_id": "20B0",
# "vendor": "NVIDIA Corporation",
# "device": "A100-SXM4-40GB",
# "driver": "nvidia",
# "pci_class": "3D controller",
# "iommu_group": "20",
# "sriov_totalvfs": 0,
# "sriov_numvfs": 0,
#
# "full_passthrough": {
# "enabled": true,
# "libvirt_address": {
# "domain": "0x0001",
# "bus": "0x65",
# "slot": "0x00",
# "function": "0x0"
# },
# "used_by_vm": "ml-train"
# },
#
# "vgpu_instances": [],
# "vf_instances": []
# }
# ]
# }
#
@ -416,9 +442,18 @@ parse_nvidia_vgpu_profiles() {
store_profile_data
gpu_address="${BASH_REMATCH[1]}"
# Convert from format like 00000000:AF:00.0 to AF:00.0 and normalize to lowercase
if [[ $gpu_address =~ [0-9A-Fa-f]+:([0-9A-Fa-f]+:[0-9A-Fa-f]+\.[0-9A-Fa-f]+) ]]; then
gpu_address="${BASH_REMATCH[1],,}"
# nvidia-smi reports addresses in the form "00000000:AF:00.0"
# (8-digit domain). Reformat to the canonical 4-digit
# "dddd:bb:ss.f" lowercase form so cache keys line up with the
# normalize_pci_address output used at lookup time. Short
# addresses ("AF:00.0") are widened by prepending domain 0.
if [[ $gpu_address =~ ^([0-9A-Fa-f]+):([0-9A-Fa-f]+):([0-9A-Fa-f]+)\.([0-9A-Fa-f]+)$ ]]; then
gpu_address=$(printf "%04x:%02x:%02x.%x" \
"0x${BASH_REMATCH[1]}" "0x${BASH_REMATCH[2]}" \
"0x${BASH_REMATCH[3]}" "0x${BASH_REMATCH[4]}")
elif [[ $gpu_address =~ ^([0-9A-Fa-f]+):([0-9A-Fa-f]+)\.([0-9A-Fa-f]+)$ ]]; then
gpu_address=$(printf "0000:%02x:%02x.%x" \
"0x${BASH_REMATCH[1]}" "0x${BASH_REMATCH[2]}" "0x${BASH_REMATCH[3]}")
else
gpu_address="${gpu_address,,}"
fi
@ -506,10 +541,58 @@ get_nvidia_profile_info() {
fi
}
# Get nodedev name for a PCI address (e.g. "00:02.0" -> "pci_0000_00_02_0")
get_nodedev_name() {
# Parse a PCI address and assign the libvirt-formatted DOMAIN/BUS/SLOT/FUNC
# values into the caller's scope. Accepts both full ("0000:00:02.0") and short
# ("00:02.0") formats; short addresses are assumed to be in PCI domain 0.
#
# IMPORTANT: bash uses dynamic scoping, so callers that don't want these four
# values to leak into the global scope MUST declare them `local` before
# invoking this function, e.g.:
# local DOMAIN BUS SLOT FUNC
# parse_pci_address "$addr"
parse_pci_address() {
local addr="$1"
echo "pci_$(echo "$addr" | sed 's/[:.]/\_/g' | sed 's/^/0000_/')"
if [[ $addr =~ ^([0-9A-Fa-f]+):([0-9A-Fa-f]+):([0-9A-Fa-f]+)\.([0-9A-Fa-f]+)$ ]]; then
DOMAIN=$(printf "0x%04x" "0x${BASH_REMATCH[1]}")
BUS=$(printf "0x%02x" "0x${BASH_REMATCH[2]}")
SLOT=$(printf "0x%02x" "0x${BASH_REMATCH[3]}")
FUNC=$(printf "0x%x" "0x${BASH_REMATCH[4]}")
elif [[ $addr =~ ^([0-9A-Fa-f]+):([0-9A-Fa-f]+)\.([0-9A-Fa-f]+)$ ]]; then
DOMAIN="0x0000"
BUS=$(printf "0x%02x" "0x${BASH_REMATCH[1]}")
SLOT=$(printf "0x%02x" "0x${BASH_REMATCH[2]}")
FUNC=$(printf "0x%x" "0x${BASH_REMATCH[3]}")
else
DOMAIN="0x0000"
BUS="0x00"
SLOT="0x00"
FUNC="0x0"
fi
}
# Normalize a PCI address to its canonical full domain-qualified form
# ("dddd:bb:ss.f", lowercase, zero-padded). Both "dddd:bb:ss.f" (full) and
# "bb:ss.f" (short, domain 0) inputs are accepted.
normalize_pci_address() {
local addr="$1"
if [[ $addr =~ ^([0-9A-Fa-f]+):([0-9A-Fa-f]+):([0-9A-Fa-f]+)\.([0-9A-Fa-f]+)$ ]]; then
printf "%04x:%02x:%02x.%x\n" \
"0x${BASH_REMATCH[1]}" "0x${BASH_REMATCH[2]}" \
"0x${BASH_REMATCH[3]}" "0x${BASH_REMATCH[4]}"
elif [[ $addr =~ ^([0-9A-Fa-f]+):([0-9A-Fa-f]+)\.([0-9A-Fa-f]+)$ ]]; then
printf "0000:%02x:%02x.%x\n" \
"0x${BASH_REMATCH[1]}" "0x${BASH_REMATCH[2]}" "0x${BASH_REMATCH[3]}"
else
echo "$addr"
fi
}
# Get nodedev name for a PCI address (e.g. "0000:00:02.0" -> "pci_0000_00_02_0").
# Short addresses are widened to domain 0 first.
get_nodedev_name() {
local addr
addr=$(normalize_pci_address "$1")
echo "pci_$(echo "$addr" | sed 's/[:.]/\_/g')"
}
# Get cached nodedev XML for a PCI address
@ -572,9 +655,12 @@ get_numa_node() {
echo "${node:--1}"
}
# Given a PCI address, return its PCI root (the toplevel bridge ID, e.g. "0000:00:03")
# Given a PCI address, return its PCI root (the toplevel bridge ID, e.g.
# "0000:00:03.0"). Both full ("0000:00:02.0") and short ("00:02.0") inputs are
# accepted; output is always domain-qualified.
get_pci_root() {
local addr="$1"
local addr
addr=$(normalize_pci_address "$1")
local xml
xml=$(get_nodedev_xml "$addr")
@ -583,21 +669,23 @@ get_pci_root() {
local parent
parent=$(echo "$xml" | xmlstarlet sel -t -v "/device/parent" 2>/dev/null || true)
if [[ -n "$parent" ]]; then
# If parent is a PCI device, recursively find its root
if [[ $parent =~ ^pci_0000_([0-9A-Fa-f]{2})_([0-9A-Fa-f]{2})_([0-9A-Fa-f])$ ]]; then
local parent_addr="${BASH_REMATCH[1]}:${BASH_REMATCH[2]}.${BASH_REMATCH[3]}"
# If parent is a PCI device, recursively find its root.
# libvirt nodedev names look like pci_<domain>_<bus>_<slot>_<func>
# where <domain> is one or more hex digits (typically 4).
if [[ $parent =~ ^pci_([0-9A-Fa-f]+)_([0-9A-Fa-f]{2})_([0-9A-Fa-f]{2})_([0-9A-Fa-f])$ ]]; then
local parent_addr="${BASH_REMATCH[1]}:${BASH_REMATCH[2]}:${BASH_REMATCH[3]}.${BASH_REMATCH[4]}"
get_pci_root "$parent_addr"
return
else
# Parent is not PCI device, so current device is the root
echo "0000:$addr"
# Parent is not a PCI device, so current device is the root
echo "$addr"
return
fi
fi
fi
# fallback
echo "0000:$addr"
echo "$addr"
}
# Build VM → hostdev maps:
@ -613,18 +701,23 @@ for VM in "${VMS[@]}"; do
continue
fi
# -- PCI hostdevs: use xmlstarlet to extract BDF for all PCI host devices --
while read -r bus slot func; do
# -- PCI hostdevs: use xmlstarlet to extract the full domain:bus:slot.func
# for all PCI host devices. libvirt's <address> element may omit the domain
# attribute, in which case we default to 0.
while IFS=: read -r dom bus slot func; do
[[ -n "$bus" && -n "$slot" && -n "$func" ]] || continue
# Format to match lspci output (e.g., 01:00.0) by padding with zeros
[[ -n "$dom" ]] || dom="0"
# Format to match lspci -D output (e.g., 0000:01:00.0) by padding with zeros
dom_fmt=$(printf "%04x" "0x$dom")
bus_fmt=$(printf "%02x" "0x$bus")
slot_fmt=$(printf "%02x" "0x$slot")
func_fmt=$(printf "%x" "0x$func")
BDF="$bus_fmt:$slot_fmt.$func_fmt"
BDF="${dom_fmt}:${bus_fmt}:${slot_fmt}.${func_fmt}"
pci_to_vm["$BDF"]="$VM"
done < <(echo "$xml" | xmlstarlet sel -T -t -m "//hostdev[@type='pci']/source/address" \
-v "substring-after(@bus, '0x')" -o " " \
-v "substring-after(@slot, '0x')" -o " " \
-v "substring-after(@domain, '0x')" -o ":" \
-v "substring-after(@bus, '0x')" -o ":" \
-v "substring-after(@slot, '0x')" -o ":" \
-v "substring-after(@function, '0x')" -n 2>/dev/null || true)
# -- MDEV hostdevs: use xmlstarlet to extract UUIDs --
@ -677,7 +770,8 @@ parse_and_add_gpu_properties() {
# Appends JSON strings for each found mdev instance to the global 'vlist' array.
# Arguments:
# $1: mdev_base_path (e.g., /sys/bus/pci/devices/.../mdev_supported_types)
# $2: bdf (e.g., 01:00.0)
# $2: bdf — short "bb:ss.f" (e.g. 01:00.0) or full
# "dddd:bb:ss.f" (e.g. 0001:65:00.0) for non-zero PCI domains
process_mdev_instances() {
local mdev_base_path="$1"
local bdf="$2"
@ -705,10 +799,8 @@ process_mdev_instances() {
local MDEV_UUID
MDEV_UUID=$(basename "$UDIR")
local DOMAIN="0x0000"
local BUS="0x${bdf:0:2}"
local SLOT="0x${bdf:3:2}"
local FUNC="0x${bdf:6:1}"
local DOMAIN BUS SLOT FUNC
parse_pci_address "$bdf"
local raw
raw="${mdev_to_vm[$MDEV_UUID]:-}"
@ -727,6 +819,10 @@ process_mdev_instances() {
# Parse nvidia-smi vgpu profiles once at the beginning
parse_nvidia_vgpu_profiles
# `lspci -nnm` keeps the existing output format for devices in PCI domain 0
# (short "bb:ss.f" form) and includes the domain prefix only for devices on
# non-zero PCI segments (multi-IIO servers, some ARM systems). The helpers
# below (parse_pci_address, normalize_pci_address) accept both formats.
mapfile -t LINES < <(lspci -nnm)
echo '{ "gpus": ['
@ -743,13 +839,18 @@ for LINE in "${LINES[@]}"; do
continue
fi
# sysfs paths always require the full domain-qualified form. PCI_ADDR may
# be short ("00:02.0") for domain 0 or full ("0001:65:00.0") otherwise, so
# we derive a separate SYSFS_ADDR just for filesystem lookups.
SYSFS_ADDR=$(normalize_pci_address "$PCI_ADDR")
# If this is a VF, skip it. It will be processed under its PF.
if [[ -e "/sys/bus/pci/devices/0000:$PCI_ADDR/physfn" ]]; then
if [[ -e "/sys/bus/pci/devices/$SYSFS_ADDR/physfn" ]]; then
continue
fi
# Only process GPU classes (3D controller)
if [[ ! "$PCI_CLASS" =~ (3D\ controller|Processing\ accelerators) ]]; then
# Only process PCI classes - 3D controller, Processing accelerators, Display controller
if [[ ! "$PCI_CLASS" =~ (3D\ controller|Processing\ accelerators|Display\ controller) ]]; then
continue
fi
@ -761,7 +862,7 @@ for LINE in "${LINES[@]}"; do
DEVICE_ID=$(sed -E 's/.*\[([0-9A-Fa-f]{4})\]$/\1/' <<<"$DEVICE_FIELD")
# Kernel driver
DRV_PATH="/sys/bus/pci/devices/0000:$PCI_ADDR/driver"
DRV_PATH="/sys/bus/pci/devices/$SYSFS_ADDR/driver"
if [[ -L $DRV_PATH ]]; then
DRIVER=$(basename "$(readlink "$DRV_PATH")")
else
@ -781,7 +882,7 @@ for LINE in "${LINES[@]}"; do
read -r TOTALVFS NUMVFS < <(get_sriov_counts "$PCI_ADDR")
# Get Physical GPU properties from its own description file, if available
PF_DESC_PATH="/sys/bus/pci/devices/0000:$PCI_ADDR/description"
PF_DESC_PATH="/sys/bus/pci/devices/$SYSFS_ADDR/description"
parse_and_add_gpu_properties "$PF_DESC_PATH"
# Save physical function's properties before they are overwritten by vGPU/VF processing
PF_MAX_INSTANCES=$MAX_INSTANCES
@ -791,25 +892,27 @@ for LINE in "${LINES[@]}"; do
PF_MAX_RESOLUTION_Y=$MAX_RESOLUTION_Y
# === full_passthrough usage ===
raw="${pci_to_vm[$PCI_ADDR]:-}"
# pci_to_vm is keyed by the full domain-qualified form, so normalize the
# lspci-style PCI_ADDR (which may be short for domain 0) before lookup.
raw="${pci_to_vm[$(normalize_pci_address "$PCI_ADDR")]:-}"
FULL_USED_JSON=$(to_json_vm "$raw")
# === vGPU (MDEV) instances ===
VGPU_ARRAY="[]"
declare -a vlist=()
# Process mdev on the Physical Function
MDEV_BASE="/sys/bus/pci/devices/0000:$PCI_ADDR/mdev_supported_types"
MDEV_BASE="/sys/bus/pci/devices/$SYSFS_ADDR/mdev_supported_types"
process_mdev_instances "$MDEV_BASE" "$PCI_ADDR"
# === VF instances (SR-IOV / MIG) ===
VF_ARRAY="[]"
declare -a flist=()
if ((TOTALVFS > 0)); then
for VF_LINK in /sys/bus/pci/devices/0000:"$PCI_ADDR"/virtfn*; do
for VF_LINK in /sys/bus/pci/devices/"$SYSFS_ADDR"/virtfn*; do
[[ -L $VF_LINK ]] || continue
VF_PATH=$(readlink -f "$VF_LINK")
VF_ADDR=${VF_PATH##*/} # e.g. "0000:65:00.2"
VF_BDF="${VF_ADDR:5}" # "65:00.2"
# Keep the full domain-qualified address (e.g. "0000:65:00.2")
VF_BDF=${VF_PATH##*/}
# For NVIDIA SR-IOV, check for vGPU (mdev) on the VF itself
if [[ "$VENDOR_ID" == "10de" ]]; then
@ -817,10 +920,7 @@ for LINE in "${LINES[@]}"; do
process_mdev_instances "$VF_MDEV_BASE" "$VF_BDF"
fi
DOMAIN="0x0000"
BUS="0x${VF_BDF:0:2}"
SLOT="0x${VF_BDF:3:2}"
FUNC="0x${VF_BDF:6:1}"
parse_pci_address "$VF_BDF"
# Determine vf_profile using nvidia-smi information
VF_PROFILE=""
@ -835,8 +935,10 @@ for LINE in "${LINES[@]}"; do
# For NVIDIA GPUs, check current vGPU type
current_vgpu_type=$(get_current_vgpu_type "$VF_PATH")
if [[ "$current_vgpu_type" != "0" ]]; then
# Get profile info from nvidia-smi cache
profile_info=$(get_nvidia_profile_info "$PCI_ADDR" "$current_vgpu_type")
# nvidia_vgpu_profiles is keyed by the canonical full
# "dddd:bb:ss.f" form, so widen PCI_ADDR (which may be
# short for domain 0) before the cache lookup.
profile_info=$(get_nvidia_profile_info "$(normalize_pci_address "$PCI_ADDR")" "$current_vgpu_type")
IFS='|' read -r VF_PROFILE_NAME VF_MAX_INSTANCES VF_VIDEO_RAM VF_MAX_HEADS VF_MAX_RESOLUTION_X VF_MAX_RESOLUTION_Y <<< "$profile_info"
VF_PROFILE="$VF_PROFILE_NAME"
fi
@ -853,12 +955,17 @@ for LINE in "${LINES[@]}"; do
fi
VF_PROFILE_JSON=$(json_escape "$VF_PROFILE")
# Determine which VM uses this VF_BDF
raw="${pci_to_vm[$VF_BDF]:-}"
# Determine which VM uses this VF_BDF (normalize for map lookup)
raw="${pci_to_vm[$(normalize_pci_address "$VF_BDF")]:-}"
USED_JSON=$(to_json_vm "$raw")
# For backward-compat JSON output, strip the default "0000:" domain
# prefix so domain-0 VFs still print as "65:00.2" rather than the
# full "0000:65:00.2". Non-zero domains are preserved unchanged.
VF_DISPLAY_ADDR="${VF_BDF#0000:}"
flist+=(
"{\"vf_pci_address\":\"$VF_BDF\",\"vf_profile\":$VF_PROFILE_JSON,\"max_instances\":$VF_MAX_INSTANCES,\"video_ram\":$VF_VIDEO_RAM,\"max_heads\":$VF_MAX_HEADS,\"max_resolution_x\":$VF_MAX_RESOLUTION_X,\"max_resolution_y\":$VF_MAX_RESOLUTION_Y,\"libvirt_address\":{\"domain\":\"$DOMAIN\",\"bus\":\"$BUS\",\"slot\":\"$SLOT\",\"function\":\"$FUNC\"},\"used_by_vm\":$USED_JSON}")
"{\"vf_pci_address\":\"$VF_DISPLAY_ADDR\",\"vf_profile\":$VF_PROFILE_JSON,\"max_instances\":$VF_MAX_INSTANCES,\"video_ram\":$VF_VIDEO_RAM,\"max_heads\":$VF_MAX_HEADS,\"max_resolution_x\":$VF_MAX_RESOLUTION_X,\"max_resolution_y\":$VF_MAX_RESOLUTION_Y,\"libvirt_address\":{\"domain\":\"$DOMAIN\",\"bus\":\"$BUS\",\"slot\":\"$SLOT\",\"function\":\"$FUNC\"},\"used_by_vm\":$USED_JSON}")
done
if [ ${#flist[@]} -gt 0 ]; then
VF_ARRAY="[$(
@ -882,10 +989,9 @@ for LINE in "${LINES[@]}"; do
if [[ ${#vlist[@]} -eq 0 && ${#flist[@]} -eq 0 ]]; then
FP_ENABLED=1
fi
DOMAIN="0x0000"
BUS="0x${PCI_ADDR:0:2}"
SLOT="0x${PCI_ADDR:3:2}"
FUNC="0x${PCI_ADDR:6:1}"
# Sets global DOMAIN/BUS/SLOT/FUNC for JSON output below
parse_pci_address "$PCI_ADDR"
# Emit JSON
if $first_gpu; then

View File

@ -52,6 +52,7 @@ import com.cloud.server.ManagementService;
import com.cloud.storage.dao.StoragePoolAndAccessGroupMapDao;
import com.cloud.cluster.ManagementServerHostPeerJoinVO;
import com.cloud.vm.UserVmManager;
import org.apache.cloudstack.acl.ControlledEntity;
import org.apache.cloudstack.acl.ControlledEntity.ACLType;
import org.apache.cloudstack.acl.SecurityChecker;
@ -4401,6 +4402,9 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q
List<String> hostTags = new ArrayList<>();
if (currentVmOffering != null) {
hostTags.addAll(com.cloud.utils.StringUtils.csvTagsToList(currentVmOffering.getHostTag()));
if (UserVmManager.AllowDifferentHostTagsOfferingsForVmScale.value()) {
addVmCurrentClusterHostTags(vmInstance, hostTags);
}
}
if (!hostTags.isEmpty()) {
@ -4412,7 +4416,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q
flag = false;
serviceOfferingSearch.op("hostTag" + tag, serviceOfferingSearch.entity().getHostTag(), Op.FIND_IN_SET);
} else {
serviceOfferingSearch.and("hostTag" + tag, serviceOfferingSearch.entity().getHostTag(), Op.FIND_IN_SET);
serviceOfferingSearch.or("hostTag" + tag, serviceOfferingSearch.entity().getHostTag(), Op.FIND_IN_SET);
}
}
serviceOfferingSearch.cp().cp();
@ -4557,6 +4561,30 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q
return new Pair<>(offeringIds, count);
}
protected void addVmCurrentClusterHostTags(VMInstanceVO vmInstance, List<String> hostTags) {
if (vmInstance == null) {
return;
}
Long hostId = vmInstance.getHostId() == null ? vmInstance.getLastHostId() : vmInstance.getHostId();
if (hostId == null) {
return;
}
HostVO host = hostDao.findById(hostId);
if (host == null) {
logger.warn("Unable to find host with id " + hostId);
return;
}
List<String> clusterTags = _hostTagDao.listByClusterId(host.getClusterId());
if (CollectionUtils.isEmpty(clusterTags)) {
logger.debug("No host tags defined for hosts in the cluster " + host.getClusterId());
return;
}
Set<String> existingTagsSet = new HashSet<>(hostTags);
clusterTags.stream()
.filter(tag -> !existingTagsSet.contains(tag))
.forEach(hostTags::add);
}
@Override
public ListResponse<ZoneResponse> listDataCenters(ListZonesCmd cmd) {
Pair<List<DataCenterJoinVO>, Integer> result = listDataCentersInternal(cmd);

View File

@ -108,6 +108,9 @@ public interface UserVmManager extends UserVmService {
"Comma separated list of allowed additional VM settings if VM instance settings are read from OVA.",
true, ConfigKey.Scope.Zone, null, null, null, null, null, ConfigKey.Kind.CSV, null);
ConfigKey<Boolean> AllowDifferentHostTagsOfferingsForVmScale = new ConfigKey<>("Advanced", Boolean.class, "allow.different.host.tags.offerings.for.vm.scale", "false",
"Enables/Disable allowing to change a VM offering to offerings with different host tags", true);
static final int MAX_USER_DATA_LENGTH_BYTES = 2048;
public static final String CKS_NODE = "cksnode";

View File

@ -9454,7 +9454,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
VmIpFetchThreadPoolMax, VmIpFetchTaskWorkers, AllowDeployVmIfGivenHostFails, EnableAdditionalVmConfig, DisplayVMOVFProperties,
KvmAdditionalConfigAllowList, XenServerAdditionalConfigAllowList, VmwareAdditionalConfigAllowList, DestroyRootVolumeOnVmDestruction,
EnforceStrictResourceLimitHostTagCheck, StrictHostTags, AllowUserForceStopVm, VmDistinctHostNameScope,
VmwareAdditionalDetailsFromOvaEnabled, VmwareAllowedAdditionalDetailsFromOva};
VmwareAdditionalDetailsFromOvaEnabled, VmwareAllowedAdditionalDetailsFromOva, AllowDifferentHostTagsOfferingsForVmScale};
}
@Override

View File

@ -1681,6 +1681,8 @@ public class BackupManagerImpl extends ManagerBase implements BackupManager {
validateBackupForZone(backup.getZoneId());
accountManager.checkAccess(CallContext.current().getCallingAccount(), null, true, vm == null ? backup : vm);
checkForPendingBackupJobs(backup);
final BackupOffering offering = backupOfferingDao.findByIdIncludingRemoved(backup.getBackupOfferingId());
if (offering == null) {
throw new CloudRuntimeException(String.format("Backup offering with ID [%s] does not exist.", backup.getBackupOfferingId()));
@ -1701,6 +1703,18 @@ public class BackupManagerImpl extends ManagerBase implements BackupManager {
throw new CloudRuntimeException("Failed to delete the backup");
}
private void checkForPendingBackupJobs(final BackupVO backup) {
String backupUuid = backup.getUuid();
long pendingJobs = asyncJobManager.countPendingJobs(backupUuid,
CreateVMFromBackupCmd.class.getName(),
CreateVMFromBackupCmdByAdmin.class.getName(),
RestoreBackupCmd.class.getName(),
RestoreVolumeFromBackupAndAttachToVMCmd.class.getName());
if (pendingJobs > 0) {
throw new CloudRuntimeException("Cannot delete Backup while a create Instance from Backup or restore Backup operation is in progress, please try again later.");
}
}
/**
* Get the pair: hostIp, datastoreUuid in which to restore the volume, based on the VM to be attached information
*/

View File

@ -18,6 +18,7 @@
package com.cloud.api.query;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
@ -33,6 +34,7 @@ import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import com.cloud.host.dao.HostTagsDao;
import org.apache.cloudstack.acl.SecurityChecker;
import org.apache.cloudstack.api.ApiCommandResourceType;
import org.apache.cloudstack.api.ResponseObject;
@ -156,6 +158,9 @@ public class QueryManagerImplTest {
@Mock
HostDao hostDao;
@Mock
HostTagsDao hostTagsDao;
@Mock
ClusterDao clusterDao;
@ -622,4 +627,39 @@ public class QueryManagerImplTest {
verify(host1).setExtensionId("a");
verify(host2).setExtensionId("b");
}
@Test
public void testAddVmCurrentClusterHostTags() {
String tag1 = "tag1";
String tag2 = "tag2";
VMInstanceVO vmInstance = mock(VMInstanceVO.class);
HostVO host = mock(HostVO.class);
when(vmInstance.getHostId()).thenReturn(null);
when(vmInstance.getLastHostId()).thenReturn(1L);
when(hostDao.findById(1L)).thenReturn(host);
when(host.getClusterId()).thenReturn(1L);
when(hostTagsDao.listByClusterId(1L)).thenReturn(Arrays.asList(tag1, tag2));
List<String> hostTags = new ArrayList<>(Collections.singleton(tag1));
queryManagerImplSpy.addVmCurrentClusterHostTags(vmInstance, hostTags);
assertEquals(2, hostTags.size());
assertTrue(hostTags.contains(tag2));
}
@Test
public void testAddVmCurrentClusterHostTagsEmptyHostTagsInCluster() {
String tag1 = "tag1";
VMInstanceVO vmInstance = mock(VMInstanceVO.class);
HostVO host = mock(HostVO.class);
when(vmInstance.getHostId()).thenReturn(null);
when(vmInstance.getLastHostId()).thenReturn(1L);
when(hostDao.findById(1L)).thenReturn(host);
when(host.getClusterId()).thenReturn(1L);
when(hostTagsDao.listByClusterId(1L)).thenReturn(null);
List<String> hostTags = new ArrayList<>(Collections.singleton(tag1));
queryManagerImplSpy.addVmCurrentClusterHostTags(vmInstance, hostTags);
assertEquals(1, hostTags.size());
assertTrue(hostTags.contains(tag1));
}
}

View File

@ -95,6 +95,7 @@ import org.apache.cloudstack.framework.config.ConfigKey;
import org.apache.cloudstack.framework.config.impl.ConfigDepotImpl;
import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao;
import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
import org.apache.cloudstack.framework.jobs.AsyncJobManager;
import org.apache.cloudstack.framework.jobs.impl.AsyncJobVO;
import org.junit.After;
import org.junit.Assert;
@ -252,6 +253,9 @@ public class BackupManagerTest {
@Mock
DomainHelper domainHelper;
@Mock
AsyncJobManager asyncJobManager;
private Gson gson;
private String[] hostPossibleValues = {"127.0.0.1", "hostname"};
@ -1505,6 +1509,7 @@ public class BackupManagerTest {
when(backup.getAccountId()).thenReturn(accountId);
when(backup.getBackupOfferingId()).thenReturn(backupOfferingId);
when(backup.getSize()).thenReturn(100L);
when(backup.getUuid()).thenReturn("backup-uuid");
overrideBackupFrameworkConfigValue();
@ -1539,6 +1544,31 @@ public class BackupManagerTest {
}
}
@Test(expected = CloudRuntimeException.class)
public void testDeleteBackupBlockedByPendingJobs() {
Long backupId = 1L;
Long vmId = 2L;
BackupVO backup = mock(BackupVO.class);
when(backup.getVmId()).thenReturn(vmId);
when(backup.getUuid()).thenReturn("backup-uuid");
when(backup.getZoneId()).thenReturn(1L);
when(backupDao.findByIdIncludingRemoved(backupId)).thenReturn(backup);
VMInstanceVO vm = mock(VMInstanceVO.class);
when(vmInstanceDao.findByIdIncludingRemoved(vmId)).thenReturn(vm);
overrideBackupFrameworkConfigValue();
when(asyncJobManager.countPendingJobs("backup-uuid",
"org.apache.cloudstack.api.command.user.vm.CreateVMFromBackupCmd",
"org.apache.cloudstack.api.command.admin.vm.CreateVMFromBackupCmdByAdmin",
"org.apache.cloudstack.api.command.user.backup.RestoreBackupCmd",
"org.apache.cloudstack.api.command.user.backup.RestoreVolumeFromBackupAndAttachToVMCmd")).thenReturn(1L);
backupManager.deleteBackup(backupId, false);
}
@Test
public void testNewBackupResponse() {
Long vmId = 1L;

View File

@ -1454,7 +1454,7 @@ export default {
this.initForm()
this.dataPreFill = this.preFillContent && Object.keys(this.preFillContent).length > 0 ? this.preFillContent : {}
this.showOverrideDiskOfferingOption = this.dataPreFill.overridediskoffering
this.selectedArchitecture = this.dataPreFill.backupArch ? this.dataPreFill.backupArch : this.architectureTypes.opts[0].id
if (this.dataPreFill.isIso) {
this.tabKey = 'isoid'
} else {
@ -1543,46 +1543,6 @@ export default {
fillValue (field) {
this.form[field] = this.dataPreFill[field]
},
fetchZoneByQuery () {
return new Promise(resolve => {
let zones = []
let apiName = ''
const params = {}
if (this.templateId) {
apiName = 'listTemplates'
params.listall = true
params.templatefilter = this.isNormalAndDomainUser ? 'executable' : 'all'
params.id = this.templateId
} else if (this.isoId) {
apiName = 'listIsos'
params.listall = true
params.isofilter = this.isNormalAndDomainUser ? 'executable' : 'all'
params.id = this.isoId
} else if (this.networkId) {
params.listall = true
params.id = this.networkId
apiName = 'listNetworks'
}
if (!apiName) return resolve(zones)
getAPI(apiName, params).then(json => {
let objectName
const responseName = [apiName.toLowerCase(), 'response'].join('')
for (const key in json[responseName]) {
if (key === 'count') {
continue
}
objectName = key
break
}
const data = json?.[responseName]?.[objectName] || []
zones = data.map(item => item.zoneid)
return resolve(zones)
}).catch(() => {
return resolve(zones)
})
})
},
async fetchData () {
this.fetchZones(null, null)
_.each(this.params, (param, name) => {
@ -1721,6 +1681,7 @@ export default {
if (template.details['vmware-to-kvm-mac-addresses']) {
this.dataPreFill.macAddressArray = JSON.parse(template.details['vmware-to-kvm-mac-addresses'])
}
this.selectedArchitecture = template?.arch || 'x86_64'
}
} else if (name === 'isoid') {
this.templateConfigurations = []
@ -2347,9 +2308,6 @@ export default {
this.clusterId = null
this.zone = _.find(this.options.zones, (option) => option.id === value)
this.isZoneSelectedMultiArch = this.zone.ismultiarch
if (this.isZoneSelectedMultiArch) {
this.selectedArchitecture = this.architectureTypes.opts[0].id
}
this.zoneSelected = true
this.form.startvm = true
this.selectedZone = this.zoneId

View File

@ -41,6 +41,8 @@
<template #headerCell="{ column }">
<template v-if="column.key === 'cpu'"><appstore-outlined /> {{ $t('label.cpu') }}</template>
<template v-if="column.key === 'ram'"><bulb-outlined /> {{ $t('label.memory') }}</template>
<template v-if="column.key === 'hosttags'"><tag-outlined /> {{ $t('label.hosttags') }}</template>
<template v-if="column.key === 'storagetags'"><tag-outlined /> {{ $t('label.storagetags') }}</template>
<template v-if="column.key === 'gpu'"><font-awesome-icon
:icon="['fa-solid', 'fa-microchip']"
class="anticon"
@ -197,6 +199,22 @@ export default {
})
}
if (this.computeItems.some(item => item.hosttags !== undefined && item.hosttags !== null)) {
baseColumns.push({
key: 'hosttags',
dataIndex: 'hosttags',
width: '30%'
})
}
if (this.computeItems.some(item => item.storagetags !== undefined && item.storagetags !== null)) {
baseColumns.push({
key: 'storagetags',
dataIndex: 'storagetags',
width: '30%'
})
}
return baseColumns
},
tableSource () {
@ -256,6 +274,7 @@ export default {
}
gpuValue = gpuCount + ' x ' + gpuType
}
return {
key: item.id,
name: item.name,
@ -267,7 +286,9 @@ export default {
gpuCount: gpuCount,
gpuType: gpuType,
gpu: gpuValue,
gpuDetails: this.getGpuDetails(item)
gpuDetails: this.getGpuDetails(item),
hosttags: item.hosttags !== undefined && item.hosttags !== null ? item.hosttags : undefined,
storagetags: item.storagetags !== undefined && item.storagetags !== null ? item.storagetags : undefined
}
})
},

View File

@ -92,10 +92,11 @@ export default {
}
},
async created () {
await Promise.all[(
await Promise.all([
this.fetchServiceOffering(),
this.fetchBackupOffering()
)]
this.fetchBackupOffering(),
this.fetchBackupArch()
])
this.loading = false
},
methods: {
@ -118,6 +119,23 @@ export default {
this.backupOffering = backupOfferings[0]
})
},
fetchBackupArch () {
const isIso = this.resource.vmdetails.isiso === 'true'
const api = isIso ? 'listIsos' : 'listTemplates'
const responseKey = isIso ? 'listisosresponse' : 'listtemplatesresponse'
const itemKey = isIso ? 'iso' : 'template'
return getAPI(api, {
id: this.resource.vmdetails.templateid,
listall: true,
...(isIso ? {} : { templatefilter: 'all' })
}).then(response => {
const items = response?.[responseKey]?.[itemKey] || []
this.backupArch = items[0]?.arch || 'x86_64'
}).catch(() => {
this.backupArch = 'x86_64'
})
},
populatePreFillData () {
this.vmdetails = this.resource.vmdetails
this.dataPreFill.zoneid = this.resource.zoneid
@ -128,6 +146,7 @@ export default {
this.dataPreFill.backupid = this.resource.id
this.dataPreFill.computeofferingid = this.vmdetails.serviceofferingid
this.dataPreFill.templateid = this.vmdetails.templateid
this.dataPreFill.backupArch = this.backupArch
this.dataPreFill.allowtemplateisoselection = true
this.dataPreFill.isoid = this.vmdetails.templateid
this.dataPreFill.allowIpAddressesFetch = this.resource.isbackupvmexpunged