From 7ba5240b311f4b66ff4d2c2ced0a14292d7cd62f Mon Sep 17 00:00:00 2001 From: Daman Arora <61474540+Damans227@users.noreply.github.com> Date: Wed, 8 Apr 2026 22:39:01 -0700 Subject: [PATCH 1/7] Block backup deletion while create-VM-from-backup or restore jobs are in progress (#12792) * Block backup deletion while create-VM-from-backup or restore jobs are in progress * Add tests * Fix exception message * Update test Co-authored-by: Abhisar Sinha <63767682+abh1sar@users.noreply.github.com> --- .../cloudstack/backup/BackupManagerImpl.java | 14 +++++++++ .../cloudstack/backup/BackupManagerTest.java | 30 +++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java b/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java index d5663ed32720..e9ed8e42eaa3 100644 --- a/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java @@ -1531,6 +1531,8 @@ public boolean deleteBackup(final Long backupId, final Boolean forced) { 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())); @@ -1551,6 +1553,18 @@ public boolean deleteBackup(final Long backupId, final Boolean forced) { 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 */ diff --git a/server/src/test/java/org/apache/cloudstack/backup/BackupManagerTest.java b/server/src/test/java/org/apache/cloudstack/backup/BackupManagerTest.java index 8b13fd474947..2c9d11612c41 100644 --- a/server/src/test/java/org/apache/cloudstack/backup/BackupManagerTest.java +++ b/server/src/test/java/org/apache/cloudstack/backup/BackupManagerTest.java @@ -91,6 +91,7 @@ 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; @@ -241,6 +242,9 @@ public class BackupManagerTest { @Mock private GuestOSDao _guestOSDao; + @Mock + AsyncJobManager asyncJobManager; + private Gson gson; private String[] hostPossibleValues = {"127.0.0.1", "hostname"}; @@ -1489,6 +1493,7 @@ public void testDeleteBackupVmNotFound() { when(backup.getAccountId()).thenReturn(accountId); when(backup.getBackupOfferingId()).thenReturn(backupOfferingId); when(backup.getSize()).thenReturn(100L); + when(backup.getUuid()).thenReturn("backup-uuid"); overrideBackupFrameworkConfigValue(); @@ -1523,6 +1528,31 @@ public void testDeleteBackupVmNotFound() { } } + @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; From 1ff9eec9977fc68f59e11644b9543177df403ab5 Mon Sep 17 00:00:00 2001 From: Manoj Kumar Date: Thu, 9 Apr 2026 13:19:49 +0530 Subject: [PATCH 2/7] Load arch data for backup from template during create instance from backup (#12801) --- ui/src/components/view/DeployVMFromBackup.vue | 46 +------------------ ui/src/views/storage/CreateVMFromBackup.vue | 25 ++++++++-- 2 files changed, 24 insertions(+), 47 deletions(-) diff --git a/ui/src/components/view/DeployVMFromBackup.vue b/ui/src/components/view/DeployVMFromBackup.vue index 55808db291c2..888d29509e1d 100644 --- a/ui/src/components/view/DeployVMFromBackup.vue +++ b/ui/src/components/view/DeployVMFromBackup.vue @@ -1451,7 +1451,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 { @@ -1540,46 +1540,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) => { @@ -1718,6 +1678,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 = [] @@ -2344,9 +2305,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 diff --git a/ui/src/views/storage/CreateVMFromBackup.vue b/ui/src/views/storage/CreateVMFromBackup.vue index 8d29397e45ad..891e8fe96420 100644 --- a/ui/src/views/storage/CreateVMFromBackup.vue +++ b/ui/src/views/storage/CreateVMFromBackup.vue @@ -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 From b5858029bb516329f5688662b2232c9595f67748 Mon Sep 17 00:00:00 2001 From: Nicolas Vazquez Date: Thu, 9 Apr 2026 05:55:47 -0300 Subject: [PATCH 3/7] Fix listing service offerings with different host tags (#12919) --- .../java/com/cloud/host/dao/HostTagsDao.java | 5 +++ .../com/cloud/host/dao/HostTagsDaoImpl.java | 20 ++++++++++ .../com/cloud/api/query/QueryManagerImpl.java | 30 +++++++++++++- .../main/java/com/cloud/vm/UserVmManager.java | 3 ++ .../java/com/cloud/vm/UserVmManagerImpl.java | 2 +- .../cloud/api/query/QueryManagerImplTest.java | 40 +++++++++++++++++++ .../wizard/ComputeOfferingSelection.vue | 23 ++++++++++- 7 files changed, 120 insertions(+), 3 deletions(-) diff --git a/engine/schema/src/main/java/com/cloud/host/dao/HostTagsDao.java b/engine/schema/src/main/java/com/cloud/host/dao/HostTagsDao.java index 7a00829fd44e..0d86ca0e48c7 100644 --- a/engine/schema/src/main/java/com/cloud/host/dao/HostTagsDao.java +++ b/engine/schema/src/main/java/com/cloud/host/dao/HostTagsDao.java @@ -45,4 +45,9 @@ public interface HostTagsDao extends GenericDao { HostTagResponse newHostTagResponse(HostTagVO hostTag); List searchByIds(Long... hostTagIds); + + /** + * List all host tags defined on hosts within a cluster + */ + List listByClusterId(Long clusterId); } diff --git a/engine/schema/src/main/java/com/cloud/host/dao/HostTagsDaoImpl.java b/engine/schema/src/main/java/com/cloud/host/dao/HostTagsDaoImpl.java index 4aa14a31cfcf..d3fee6a26761 100644 --- a/engine/schema/src/main/java/com/cloud/host/dao/HostTagsDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/host/dao/HostTagsDaoImpl.java @@ -23,6 +23,7 @@ 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 implements private final SearchBuilder stSearch; private final SearchBuilder tagIdsearch; private final SearchBuilder ImplicitTagsSearch; + private final GenericSearchBuilder tagSearch; @Inject private ConfigurationDao _configDao; + @Inject + private HostDao hostDao; public HostTagsDaoImpl() { HostSearch = createSearchBuilder(); @@ -72,6 +76,11 @@ public HostTagsDaoImpl() { 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 List searchByIds(Long... tagIds) { return tagList; } + + @Override + public List listByClusterId(Long clusterId) { + List hostIds = hostDao.listIdsByClusterId(clusterId); + if (CollectionUtils.isEmpty(hostIds)) { + return new ArrayList<>(); + } + SearchCriteria sc = tagSearch.create(); + sc.setParameters("hostIdIN", hostIds.toArray()); + return customSearch(sc, null); + } } diff --git a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java index ac9f8ee1433c..fea87b66fed8 100644 --- a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java +++ b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java @@ -45,6 +45,7 @@ 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; @@ -4330,6 +4331,9 @@ private Pair, Integer> searchForServiceOfferingIdsAndCount(ListServic List 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()) { @@ -4341,7 +4345,7 @@ private Pair, Integer> searchForServiceOfferingIdsAndCount(ListServic 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(); @@ -4486,6 +4490,30 @@ private Pair, Integer> searchForServiceOfferingIdsAndCount(ListServic return new Pair<>(offeringIds, count); } + protected void addVmCurrentClusterHostTags(VMInstanceVO vmInstance, List 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 clusterTags = _hostTagDao.listByClusterId(host.getClusterId()); + if (CollectionUtils.isEmpty(clusterTags)) { + logger.debug("No host tags defined for hosts in the cluster " + host.getClusterId()); + return; + } + Set existingTagsSet = new HashSet<>(hostTags); + clusterTags.stream() + .filter(tag -> !existingTagsSet.contains(tag)) + .forEach(hostTags::add); + } + @Override public ListResponse listDataCenters(ListZonesCmd cmd) { Pair, Integer> result = listDataCentersInternal(cmd); diff --git a/server/src/main/java/com/cloud/vm/UserVmManager.java b/server/src/main/java/com/cloud/vm/UserVmManager.java index 0a744709644c..38cb6d2db46b 100644 --- a/server/src/main/java/com/cloud/vm/UserVmManager.java +++ b/server/src/main/java/com/cloud/vm/UserVmManager.java @@ -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 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"; diff --git a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java index 65bd285ca906..9cec033d07c7 100644 --- a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java +++ b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java @@ -9412,7 +9412,7 @@ public ConfigKey[] getConfigKeys() { VmIpFetchThreadPoolMax, VmIpFetchTaskWorkers, AllowDeployVmIfGivenHostFails, EnableAdditionalVmConfig, DisplayVMOVFProperties, KvmAdditionalConfigAllowList, XenServerAdditionalConfigAllowList, VmwareAdditionalConfigAllowList, DestroyRootVolumeOnVmDestruction, EnforceStrictResourceLimitHostTagCheck, StrictHostTags, AllowUserForceStopVm, VmDistinctHostNameScope, - VmwareAdditionalDetailsFromOvaEnabled, VmwareAllowedAdditionalDetailsFromOva}; + VmwareAdditionalDetailsFromOvaEnabled, VmwareAllowedAdditionalDetailsFromOva, AllowDifferentHostTagsOfferingsForVmScale}; } @Override diff --git a/server/src/test/java/com/cloud/api/query/QueryManagerImplTest.java b/server/src/test/java/com/cloud/api/query/QueryManagerImplTest.java index 892cd1e7def6..750f4d8655b1 100644 --- a/server/src/test/java/com/cloud/api/query/QueryManagerImplTest.java +++ b/server/src/test/java/com/cloud/api/query/QueryManagerImplTest.java @@ -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.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 void updateHostsExtensions_withHostResponses_setsExtension() { 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 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 hostTags = new ArrayList<>(Collections.singleton(tag1)); + queryManagerImplSpy.addVmCurrentClusterHostTags(vmInstance, hostTags); + assertEquals(1, hostTags.size()); + assertTrue(hostTags.contains(tag1)); + } } diff --git a/ui/src/views/compute/wizard/ComputeOfferingSelection.vue b/ui/src/views/compute/wizard/ComputeOfferingSelection.vue index eb6e228a93f6..0a3e5fd2e827 100644 --- a/ui/src/views/compute/wizard/ComputeOfferingSelection.vue +++ b/ui/src/views/compute/wizard/ComputeOfferingSelection.vue @@ -41,6 +41,8 @@