This is an automated email from the ASF dual-hosted git repository.

dahn pushed a commit to branch 4.22
in repository https://gitbox.apache.org/repos/asf/cloudstack.git


The following commit(s) were added to refs/heads/4.22 by this push:
     new b5858029bb5 Fix listing service offerings with different host tags 
(#12919)
b5858029bb5 is described below

commit b5858029bb516329f5688662b2232c9595f67748
Author: Nicolas Vazquez <[email protected]>
AuthorDate: Thu Apr 9 05:55:47 2026 -0300

    Fix listing service offerings with different host tags (#12919)
---
 .../main/java/com/cloud/host/dao/HostTagsDao.java  |  5 +++
 .../java/com/cloud/host/dao/HostTagsDaoImpl.java   | 20 +++++++++++
 .../java/com/cloud/api/query/QueryManagerImpl.java | 30 +++++++++++++++-
 .../src/main/java/com/cloud/vm/UserVmManager.java  |  3 ++
 .../main/java/com/cloud/vm/UserVmManagerImpl.java  |  2 +-
 .../com/cloud/api/query/QueryManagerImplTest.java  | 40 ++++++++++++++++++++++
 .../compute/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 7a00829fd44..0d86ca0e48c 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<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);
 }
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 4aa14a31cfc..d3fee6a2676 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.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);
+    }
 }
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 ac9f8ee1433..fea87b66fed 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.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;
@@ -4330,6 +4331,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()) {
@@ -4341,7 +4345,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();
@@ -4486,6 +4490,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);
diff --git a/server/src/main/java/com/cloud/vm/UserVmManager.java 
b/server/src/main/java/com/cloud/vm/UserVmManager.java
index 0a744709644..38cb6d2db46 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<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";
diff --git a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java 
b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java
index 65bd285ca90..9cec033d07c 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 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
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 892cd1e7def..750f4d8655b 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.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));
+    }
 }
diff --git a/ui/src/views/compute/wizard/ComputeOfferingSelection.vue 
b/ui/src/views/compute/wizard/ComputeOfferingSelection.vue
index eb6e228a93f..0a3e5fd2e82 100644
--- a/ui/src/views/compute/wizard/ComputeOfferingSelection.vue
+++ b/ui/src/views/compute/wizard/ComputeOfferingSelection.vue
@@ -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
         }
       })
     },

Reply via email to