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

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


The following commit(s) were added to refs/heads/main by this push:
     new 5433e775e53 New feature: Implicit host tags (#8929)
5433e775e53 is described below

commit 5433e775e53ad3709db6286cb7b0a78703c823a6
Author: Wei Zhou <[email protected]>
AuthorDate: Thu May 30 13:51:13 2024 +0200

    New feature: Implicit host tags (#8929)
    
    * Merge two HostTagVO and HostTagDaoImpl
    
    * Implicit host tags
    
    * PR8929: add since
    
    * Update variable names
    
    * Update 8929: add unit test in LibvirtComputingResourceTest
    
    * Update 8929: add explicithosttags in response
    
    * Update 8929 UI: Update explicit host tags
    
    * Update 8929: remove host tags and change labels on UI
    
    * Update 8929: update host_view to use explicit_host_tags.is_tag_a_rule
    
    * Update: ui polish for host tags
    
    * Update 8929: fix UI error if no host tags
---
 agent/conf/agent.properties                        |   3 +
 .../cloud/agent/properties/AgentProperties.java    |   7 +
 .../org/apache/cloudstack/api/ApiConstants.java    |   1 +
 .../api/response/HostForMigrationResponse.java     |  16 ++
 .../cloudstack/api/response/HostResponse.java      |  24 +++
 .../cloudstack/api/response/HostTagResponse.java   |  13 ++
 .../com/cloud/agent/api/StartupRoutingCommand.java |   4 +
 .../src/main/java/com/cloud/host/HostTagVO.java    |  10 ++
 .../main/java/com/cloud/host/dao/HostTagsDao.java  |   8 +
 .../java/com/cloud/host/dao/HostTagsDaoImpl.java   | 125 ++++++++++++++
 .../spring-engine-schema-core-daos-context.xml     |   1 -
 .../resources/META-INF/db/schema-41900to42000.sql  |   3 +
 .../META-INF/db/views/cloud.host_view.sql          |   8 +-
 .../kvm/resource/LibvirtComputingResource.java     |  14 ++
 .../kvm/resource/LibvirtComputingResourceTest.java |  36 ++++
 server/src/main/java/com/cloud/api/ApiDBUtils.java |   8 +-
 .../java/com/cloud/api/query/QueryManagerImpl.java |  12 +-
 .../com/cloud/api/query/ViewResponseHelper.java    |   2 +-
 .../com/cloud/api/query/dao/HostJoinDaoImpl.java   |   3 +
 .../java/com/cloud/api/query/dao/HostTagDao.java   |  30 ----
 .../com/cloud/api/query/dao/HostTagDaoImpl.java    | 122 --------------
 .../java/com/cloud/api/query/vo/HostJoinVO.java    |  14 ++
 .../java/com/cloud/api/query/vo/HostTagVO.java     |  61 -------
 .../com/cloud/resource/ResourceManagerImpl.java    |  19 +--
 test/integration/smoke/test_host_tags.py           | 160 ++++++++++++++++++
 ui/public/locales/en.json                          |   7 +
 ui/src/config/section/infra/hosts.js               |   8 +-
 ui/src/views/infra/HostInfo.vue                    |  26 ++-
 ui/src/views/infra/HostUpdate.vue                  | 183 +++++++++++++++++++++
 29 files changed, 677 insertions(+), 251 deletions(-)

diff --git a/agent/conf/agent.properties b/agent/conf/agent.properties
index e600e8f8f20..3b6a7b7de29 100644
--- a/agent/conf/agent.properties
+++ b/agent/conf/agent.properties
@@ -430,3 +430,6 @@ iscsi.session.cleanup.enabled=false
 # If set to "true", the agent will register for libvirt domain events, 
allowing for immediate updates on crashed or
 # unexpectedly stopped. Experimental, requires agent restart.
 # libvirt.events.enabled=false
+
+# Implicit host tags managed by agent.properties
+# host.tags=
diff --git 
a/agent/src/main/java/com/cloud/agent/properties/AgentProperties.java 
b/agent/src/main/java/com/cloud/agent/properties/AgentProperties.java
index 24a09ae2ac1..b27ba651e4f 100644
--- a/agent/src/main/java/com/cloud/agent/properties/AgentProperties.java
+++ b/agent/src/main/java/com/cloud/agent/properties/AgentProperties.java
@@ -803,6 +803,13 @@ public class AgentProperties{
      */
     public static final Property<String> KEYSTORE_PASSPHRASE = new 
Property<>(KeyStoreUtils.KS_PASSPHRASE_PROPERTY, null, String.class);
 
+    /**
+     * Implicit host tags
+     * Data type: String.<br>
+     * Default value: <code>null</code>
+     */
+    public static final Property<String> HOST_TAGS = new 
Property<>("host.tags", null, String.class);
+
     public static class Property <T>{
         private String name;
         private T defaultValue;
diff --git a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java 
b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java
index 4115d440d78..c5a059c39be 100644
--- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java
+++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java
@@ -265,6 +265,7 @@ public class ApiConstants {
     public static final String IS_EDGE = "isedge";
     public static final String IS_EXTRACTABLE = "isextractable";
     public static final String IS_FEATURED = "isfeatured";
+    public static final String IS_IMPLICIT = "isimplicit";
     public static final String IS_PORTABLE = "isportable";
     public static final String IS_PUBLIC = "ispublic";
     public static final String IS_PERSISTENT = "ispersistent";
diff --git 
a/api/src/main/java/org/apache/cloudstack/api/response/HostForMigrationResponse.java
 
b/api/src/main/java/org/apache/cloudstack/api/response/HostForMigrationResponse.java
index 41a0fdc4567..24015e0b459 100644
--- 
a/api/src/main/java/org/apache/cloudstack/api/response/HostForMigrationResponse.java
+++ 
b/api/src/main/java/org/apache/cloudstack/api/response/HostForMigrationResponse.java
@@ -208,6 +208,14 @@ public class HostForMigrationResponse extends BaseResponse 
{
     @Param(description = "comma-separated list of tags for the host")
     private String hostTags;
 
+    @SerializedName("explicithosttags")
+    @Param(description = "comma-separated list of explicit host tags for the 
host", since = "4.20.0")
+    private String explicitHostTags;
+
+    @SerializedName("implicithosttags")
+    @Param(description = "comma-separated list of implicit host tags for the 
host", since = "4.20.0")
+    private String implicitHostTags;
+
     @SerializedName("hasenoughcapacity")
     @Param(description = "true if this host has enough CPU and RAM capacity to 
migrate a VM to it, false otherwise")
     private Boolean hasEnoughCapacity;
@@ -414,6 +422,14 @@ public class HostForMigrationResponse extends BaseResponse 
{
         this.hostTags = hostTags;
     }
 
+    public void setExplicitHostTags(String explicitHostTags) {
+        this.explicitHostTags = explicitHostTags;
+    }
+
+    public void setImplicitHostTags(String implicitHostTags) {
+        this.implicitHostTags = implicitHostTags;
+    }
+
     public void setHasEnoughCapacity(Boolean hasEnoughCapacity) {
         this.hasEnoughCapacity = hasEnoughCapacity;
     }
diff --git 
a/api/src/main/java/org/apache/cloudstack/api/response/HostResponse.java 
b/api/src/main/java/org/apache/cloudstack/api/response/HostResponse.java
index d72d23b99c9..3a88b819572 100644
--- a/api/src/main/java/org/apache/cloudstack/api/response/HostResponse.java
+++ b/api/src/main/java/org/apache/cloudstack/api/response/HostResponse.java
@@ -221,6 +221,14 @@ public class HostResponse extends 
BaseResponseWithAnnotations {
     @Param(description = "comma-separated list of tags for the host")
     private String hostTags;
 
+    @SerializedName("explicithosttags")
+    @Param(description = "comma-separated list of explicit host tags for the 
host", since = "4.20.0")
+    private String explicitHostTags;
+
+    @SerializedName("implicithosttags")
+    @Param(description = "comma-separated list of implicit host tags for the 
host", since = "4.20.0")
+    private String implicitHostTags;
+
     @SerializedName(ApiConstants.IS_TAG_A_RULE)
     @Param(description = ApiConstants.PARAMETER_DESCRIPTION_IS_TAG_A_RULE)
     private Boolean isTagARule;
@@ -458,6 +466,22 @@ public class HostResponse extends 
BaseResponseWithAnnotations {
         this.hostTags = hostTags;
     }
 
+    public String getExplicitHostTags() {
+        return explicitHostTags;
+    }
+
+    public void setExplicitHostTags(String explicitHostTags) {
+        this.explicitHostTags = explicitHostTags;
+    }
+
+    public String getImplicitHostTags() {
+        return implicitHostTags;
+    }
+
+    public void setImplicitHostTags(String implicitHostTags) {
+        this.implicitHostTags = implicitHostTags;
+    }
+
     public void setHasEnoughCapacity(Boolean hasEnoughCapacity) {
         this.hasEnoughCapacity = hasEnoughCapacity;
     }
diff --git 
a/api/src/main/java/org/apache/cloudstack/api/response/HostTagResponse.java 
b/api/src/main/java/org/apache/cloudstack/api/response/HostTagResponse.java
index 4a924ea78a0..f772da6dcb6 100644
--- a/api/src/main/java/org/apache/cloudstack/api/response/HostTagResponse.java
+++ b/api/src/main/java/org/apache/cloudstack/api/response/HostTagResponse.java
@@ -19,6 +19,7 @@ package org.apache.cloudstack.api.response;
 import com.google.gson.annotations.SerializedName;
 import com.cloud.serializer.Param;
 
+import org.apache.cloudstack.api.ApiConstants;
 import org.apache.cloudstack.api.BaseResponse;
 
 public class HostTagResponse extends BaseResponse {
@@ -34,6 +35,10 @@ public class HostTagResponse extends BaseResponse {
     @Param(description = "the name of the host tag")
     private String name;
 
+    @SerializedName(ApiConstants.IS_IMPLICIT)
+    @Param(description = "true if the host tag is implicit", since = "4.20.0")
+    private boolean isImplicit;
+
     public String getId() {
         return id;
     }
@@ -57,4 +62,12 @@ public class HostTagResponse extends BaseResponse {
     public void setName(String name) {
         this.name = name;
     }
+
+    public boolean isImplicit() {
+        return isImplicit;
+    }
+
+    public void setImplicit(boolean implicit) {
+        isImplicit = implicit;
+    }
 }
diff --git a/core/src/main/java/com/cloud/agent/api/StartupRoutingCommand.java 
b/core/src/main/java/com/cloud/agent/api/StartupRoutingCommand.java
index b4f9d20df5e..2d4ed8c9cc4 100644
--- a/core/src/main/java/com/cloud/agent/api/StartupRoutingCommand.java
+++ b/core/src/main/java/com/cloud/agent/api/StartupRoutingCommand.java
@@ -174,6 +174,10 @@ public class StartupRoutingCommand extends StartupCommand {
         this.hostTags.add(hostTag);
     }
 
+    public void setHostTags(List<String> hostTags) {
+        this.hostTags = hostTags;
+    }
+
     public  HashMap<String, HashMap<String, VgpuTypesInfo>> 
getGpuGroupDetails() {
         return groupDetails;
     }
diff --git a/engine/schema/src/main/java/com/cloud/host/HostTagVO.java 
b/engine/schema/src/main/java/com/cloud/host/HostTagVO.java
index cd4ac29738d..98071a2c073 100644
--- a/engine/schema/src/main/java/com/cloud/host/HostTagVO.java
+++ b/engine/schema/src/main/java/com/cloud/host/HostTagVO.java
@@ -40,6 +40,9 @@ public class HostTagVO implements InternalIdentity {
     @Column(name = "tag")
     private String tag;
 
+    @Column(name = "is_implicit")
+    private boolean isImplicit = false;
+
     @Column(name = "is_tag_a_rule")
     private boolean isTagARule;
 
@@ -74,6 +77,13 @@ public class HostTagVO implements InternalIdentity {
         return isTagARule;
     }
 
+    public void setIsImplicit(boolean isImplicit) {
+        this.isImplicit = isImplicit;
+    }
+
+    public boolean getIsImplicit() {
+        return isImplicit;
+    }
 
     @Override
     public long getId() {
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 d134db33403..7a00829fd44 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
@@ -20,6 +20,7 @@ import java.util.List;
 
 import com.cloud.host.HostTagVO;
 import com.cloud.utils.db.GenericDao;
+import org.apache.cloudstack.api.response.HostTagResponse;
 import org.apache.cloudstack.framework.config.ConfigKey;
 
 public interface HostTagsDao extends GenericDao<HostTagVO, Long> {
@@ -35,6 +36,13 @@ public interface HostTagsDao extends GenericDao<HostTagVO, 
Long> {
 
     void deleteTags(long hostId);
 
+    boolean updateImplicitTags(long hostId, List<String> hostTags);
+
+    List<HostTagVO> getExplicitHostTags(long hostId);
+
     List<HostTagVO> findHostRuleTags();
 
+    HostTagResponse newHostTagResponse(HostTagVO hostTag);
+
+    List<HostTagVO> searchByIds(Long... hostTagIds);
 }
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 65deb1d1c9b..4aa14a31cfc 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
@@ -16,10 +16,14 @@
 // under the License.
 package com.cloud.host.dao;
 
+import java.util.ArrayList;
 import java.util.List;
 
+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.lang3.StringUtils;
 import org.springframework.stereotype.Component;
 
 import com.cloud.host.HostTagVO;
@@ -30,14 +34,23 @@ import com.cloud.utils.db.SearchCriteria;
 import com.cloud.utils.db.TransactionLegacy;
 import com.cloud.utils.db.SearchCriteria.Func;
 
+import javax.inject.Inject;
+
 @Component
 public class HostTagsDaoImpl extends GenericDaoBase<HostTagVO, Long> 
implements HostTagsDao, Configurable {
     protected final SearchBuilder<HostTagVO> HostSearch;
     protected final GenericSearchBuilder<HostTagVO, String> 
DistinctImplictTagsSearch;
+    private final SearchBuilder<HostTagVO> stSearch;
+    private final SearchBuilder<HostTagVO> tagIdsearch;
+    private final SearchBuilder<HostTagVO> ImplicitTagsSearch;
+
+    @Inject
+    private ConfigurationDao _configDao;
 
     public HostTagsDaoImpl() {
         HostSearch = createSearchBuilder();
         HostSearch.and("hostId", HostSearch.entity().getHostId(), 
SearchCriteria.Op.EQ);
+        HostSearch.and("isImplicit", HostSearch.entity().getIsImplicit(), 
SearchCriteria.Op.EQ);
         HostSearch.and("isTagARule", HostSearch.entity().getIsTagARule(), 
SearchCriteria.Op.EQ);
         HostSearch.done();
 
@@ -46,6 +59,19 @@ public class HostTagsDaoImpl extends 
GenericDaoBase<HostTagVO, Long> implements
         DistinctImplictTagsSearch.and("hostIds", 
DistinctImplictTagsSearch.entity().getHostId(), SearchCriteria.Op.IN);
         DistinctImplictTagsSearch.and("implicitTags", 
DistinctImplictTagsSearch.entity().getTag(), SearchCriteria.Op.IN);
         DistinctImplictTagsSearch.done();
+
+        stSearch = createSearchBuilder();
+        stSearch.and("idIN", stSearch.entity().getId(), SearchCriteria.Op.IN);
+        stSearch.done();
+
+        tagIdsearch = createSearchBuilder();
+        tagIdsearch.and("id", tagIdsearch.entity().getId(), 
SearchCriteria.Op.EQ);
+        tagIdsearch.done();
+
+        ImplicitTagsSearch = createSearchBuilder();
+        ImplicitTagsSearch.and("hostId", 
ImplicitTagsSearch.entity().getHostId(), SearchCriteria.Op.EQ);
+        ImplicitTagsSearch.and("isImplicit", 
ImplicitTagsSearch.entity().getIsImplicit(), SearchCriteria.Op.EQ);
+        ImplicitTagsSearch.done();
     }
 
     @Override
@@ -74,6 +100,36 @@ public class HostTagsDaoImpl extends 
GenericDaoBase<HostTagVO, Long> implements
         txn.commit();
     }
 
+    @Override
+    public boolean updateImplicitTags(long hostId, List<String> hostTags) {
+        TransactionLegacy txn = TransactionLegacy.currentTxn();
+        txn.start();
+        SearchCriteria<HostTagVO> sc = ImplicitTagsSearch.create();
+        sc.setParameters("hostId", hostId);
+        sc.setParameters("isImplicit", true);
+        boolean expunged = expunge(sc) > 0;
+        boolean persisted = false;
+        for (String tag : hostTags) {
+            if (StringUtils.isNotBlank(tag)) {
+                HostTagVO vo = new HostTagVO(hostId, tag.trim());
+                vo.setIsImplicit(true);
+                persist(vo);
+                persisted = true;
+            }
+        }
+        txn.commit();
+        return expunged || persisted;
+    }
+
+    @Override
+    public List<HostTagVO> getExplicitHostTags(long hostId) {
+        SearchCriteria<HostTagVO> sc = ImplicitTagsSearch.create();
+        sc.setParameters("hostId", hostId);
+        sc.setParameters("isImplicit", false);
+
+        return search(sc, null);
+    }
+
     @Override
     public List<HostTagVO> findHostRuleTags() {
         SearchCriteria<HostTagVO> sc = HostSearch.create();
@@ -89,6 +145,7 @@ public class HostTagsDaoImpl extends 
GenericDaoBase<HostTagVO, Long> implements
         txn.start();
         SearchCriteria<HostTagVO> sc = HostSearch.create();
         sc.setParameters("hostId", hostId);
+        sc.setParameters("isImplicit", false);
         expunge(sc);
 
         for (String tag : hostTags) {
@@ -110,4 +167,72 @@ public class HostTagsDaoImpl extends 
GenericDaoBase<HostTagVO, Long> implements
     public String getConfigComponentName() {
         return HostTagsDaoImpl.class.getSimpleName();
     }
+
+    @Override
+    public HostTagResponse newHostTagResponse(HostTagVO tag) {
+        HostTagResponse tagResponse = new HostTagResponse();
+
+        tagResponse.setName(tag.getTag());
+        tagResponse.setHostId(tag.getHostId());
+        tagResponse.setImplicit(tag.getIsImplicit());
+
+        tagResponse.setObjectName("hosttag");
+
+        return tagResponse;
+    }
+
+    @Override
+    public List<HostTagVO> searchByIds(Long... tagIds) {
+        String batchCfg = _configDao.getValue("detail.batch.query.size");
+
+        final int detailsBatchSize = batchCfg != null ? 
Integer.parseInt(batchCfg) : 2000;
+
+        // query details by batches
+        List<HostTagVO> tagList = new ArrayList<>();
+        int curr_index = 0;
+
+        if (tagIds.length > detailsBatchSize) {
+            while ((curr_index + detailsBatchSize) <= tagIds.length) {
+                Long[] ids = new Long[detailsBatchSize];
+
+                for (int k = 0, j = curr_index; j < curr_index + 
detailsBatchSize; j++, k++) {
+                    ids[k] = tagIds[j];
+                }
+
+                SearchCriteria<HostTagVO> sc = stSearch.create();
+
+                sc.setParameters("idIN", (Object[])ids);
+
+                List<HostTagVO> vms = searchIncludingRemoved(sc, null, null, 
false);
+
+                if (vms != null) {
+                    tagList.addAll(vms);
+                }
+
+                curr_index += detailsBatchSize;
+            }
+        }
+
+        if (curr_index < tagIds.length) {
+            int batch_size = (tagIds.length - curr_index);
+            // set the ids value
+            Long[] ids = new Long[batch_size];
+
+            for (int k = 0, j = curr_index; j < curr_index + batch_size; j++, 
k++) {
+                ids[k] = tagIds[j];
+            }
+
+            SearchCriteria<HostTagVO> sc = stSearch.create();
+
+            sc.setParameters("idIN", (Object[])ids);
+
+            List<HostTagVO> tags = searchIncludingRemoved(sc, null, null, 
false);
+
+            if (tags != null) {
+                tagList.addAll(tags);
+            }
+        }
+
+        return tagList;
+    }
 }
diff --git 
a/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml
 
b/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml
index c70c6d4334e..8ab60a76624 100644
--- 
a/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml
+++ 
b/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml
@@ -187,7 +187,6 @@
   <bean id="storageNetworkIpAddressDaoImpl" 
class="com.cloud.dc.dao.StorageNetworkIpAddressDaoImpl" />
   <bean id="storageNetworkIpRangeDaoImpl" 
class="com.cloud.dc.dao.StorageNetworkIpRangeDaoImpl" />
   <bean id="storagePoolJoinDaoImpl" 
class="com.cloud.api.query.dao.StoragePoolJoinDaoImpl" />
-  <bean id="hostTagDaoImpl" class="com.cloud.api.query.dao.HostTagDaoImpl" />
   <bean id="storagePoolWorkDaoImpl" 
class="com.cloud.storage.dao.StoragePoolWorkDaoImpl" />
   <bean id="uploadDaoImpl" class="com.cloud.storage.dao.UploadDaoImpl" />
   <bean id="usageDaoImpl" class="com.cloud.usage.dao.UsageDaoImpl" />
diff --git 
a/engine/schema/src/main/resources/META-INF/db/schema-41900to42000.sql 
b/engine/schema/src/main/resources/META-INF/db/schema-41900to42000.sql
index 1bb1905443a..85635ec9d0a 100644
--- a/engine/schema/src/main/resources/META-INF/db/schema-41900to42000.sql
+++ b/engine/schema/src/main/resources/META-INF/db/schema-41900to42000.sql
@@ -79,3 +79,6 @@ CREATE TABLE IF NOT EXISTS 
`cloud_usage`.`quota_email_configuration`(
     PRIMARY KEY (`account_id`, `email_template_id`),
     CONSTRAINT `FK_quota_email_configuration_account_id` FOREIGN KEY 
(`account_id`) REFERENCES `cloud_usage`.`quota_account`(`account_id`),
     CONSTRAINT `FK_quota_email_configuration_email_template_id` FOREIGN KEY 
(`email_template_id`) REFERENCES `cloud_usage`.`quota_email_templates`(`id`));
+
+-- Add `is_implicit` column to `host_tags` table
+CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.host_tags', 'is_implicit', 'int(1) 
UNSIGNED NOT NULL DEFAULT 0 COMMENT "If host tag is implicit or explicit" ');
diff --git 
a/engine/schema/src/main/resources/META-INF/db/views/cloud.host_view.sql 
b/engine/schema/src/main/resources/META-INF/db/views/cloud.host_view.sql
index 5c6d4fd772b..7bd4b3cc4a9 100644
--- a/engine/schema/src/main/resources/META-INF/db/views/cloud.host_view.sql
+++ b/engine/schema/src/main/resources/META-INF/db/views/cloud.host_view.sql
@@ -53,7 +53,9 @@ SELECT
     host_pod_ref.uuid pod_uuid,
     host_pod_ref.name pod_name,
     GROUP_CONCAT(DISTINCT(host_tags.tag)) AS tag,
-    `host_tags`.`is_tag_a_rule` AS `is_tag_a_rule`,
+    GROUP_CONCAT(DISTINCT(explicit_host_tags.tag)) AS explicit_tag,
+    GROUP_CONCAT(DISTINCT(implicit_host_tags.tag)) AS implicit_tag,
+    `explicit_host_tags`.`is_tag_a_rule` AS `is_tag_a_rule`,
     guest_os_category.id guest_os_category_id,
     guest_os_category.uuid guest_os_category_uuid,
     guest_os_category.name guest_os_category_name,
@@ -89,6 +91,10 @@ FROM
         LEFT JOIN
     `cloud`.`host_tags` ON host_tags.host_id = host.id
         LEFT JOIN
+    `cloud`.`host_tags` AS explicit_host_tags ON explicit_host_tags.host_id = 
host.id AND explicit_host_tags.is_implicit = 0
+        LEFT JOIN
+    `cloud`.`host_tags` AS implicit_host_tags ON implicit_host_tags.host_id = 
host.id AND implicit_host_tags.is_implicit = 1
+        LEFT JOIN
     `cloud`.`op_host_capacity` mem_caps ON host.id = mem_caps.host_id
         AND mem_caps.capacity_type = 0
         LEFT JOIN
diff --git 
a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java
 
b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java
index 5eed56806b8..b5ec716e805 100644
--- 
a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java
+++ 
b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java
@@ -3646,6 +3646,7 @@ public class LibvirtComputingResource extends 
ServerResourceBase implements Serv
         cmd.setGatewayIpAddress(localGateway);
         cmd.setIqn(getIqn());
         cmd.getHostDetails().put(HOST_VOLUME_ENCRYPTION, 
String.valueOf(hostSupportsVolumeEncryption()));
+        cmd.setHostTags(getHostTags());
         HealthCheckResult healthCheckResult = getHostHealthCheckResult();
         if (healthCheckResult != HealthCheckResult.IGNORE) {
             cmd.setHostHealthCheckResult(healthCheckResult == 
HealthCheckResult.SUCCESS);
@@ -3674,6 +3675,19 @@ public class LibvirtComputingResource extends 
ServerResourceBase implements Serv
         return startupCommandsArray;
     }
 
+    protected List<String> getHostTags() {
+        List<String> hostTagsList = new ArrayList<>();
+        String hostTags = 
AgentPropertiesFileHandler.getPropertyValue(AgentProperties.HOST_TAGS);
+        if (StringUtils.isNotBlank(hostTags)) {
+            for (String hostTag : hostTags.split(",")) {
+                if (!hostTagsList.contains(hostTag.trim())) {
+                    hostTagsList.add(hostTag.trim());
+                }
+            }
+        }
+        return hostTagsList;
+    }
+
     /**
      * Calculates and sets the host CPU max capacity according to the cgroup 
version of the host.
      * <ul>
diff --git 
a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResourceTest.java
 
b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResourceTest.java
index 19515ac8361..ecb34adc6ed 100644
--- 
a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResourceTest.java
+++ 
b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResourceTest.java
@@ -6295,4 +6295,40 @@ public class LibvirtComputingResourceTest {
             Assert.assertEquals(expectedShares, 
libvirtComputingResourceSpy.getHostCpuMaxCapacity());
         }
     }
+
+    @Test
+    public void testGetHostTags() throws ConfigurationException {
+        try (MockedStatic<AgentPropertiesFileHandler> ignored = 
Mockito.mockStatic(AgentPropertiesFileHandler.class)) {
+            
Mockito.when(AgentPropertiesFileHandler.getPropertyValue(Mockito.eq(AgentProperties.HOST_TAGS)))
+                    .thenReturn("aa,bb,cc,dd");
+
+            List<String> hostTagsList = 
libvirtComputingResourceSpy.getHostTags();
+            Assert.assertEquals(4, hostTagsList.size());
+            Assert.assertEquals("aa,bb,cc,dd", StringUtils.join(hostTagsList, 
","));
+        }
+    }
+
+    @Test
+    public void testGetHostTagsWithSpace() throws ConfigurationException {
+        try (MockedStatic<AgentPropertiesFileHandler> ignored = 
Mockito.mockStatic(AgentPropertiesFileHandler.class)) {
+            
Mockito.when(AgentPropertiesFileHandler.getPropertyValue(Mockito.eq(AgentProperties.HOST_TAGS)))
+                    .thenReturn(" aa, bb , cc , dd ");
+
+            List<String> hostTagsList = 
libvirtComputingResourceSpy.getHostTags();
+            Assert.assertEquals(4, hostTagsList.size());
+            Assert.assertEquals("aa,bb,cc,dd", StringUtils.join(hostTagsList, 
","));
+        }
+    }
+
+    @Test
+    public void testGetHostTagsWithEmptyPropertyValue() throws 
ConfigurationException {
+        try (MockedStatic<AgentPropertiesFileHandler> ignored = 
Mockito.mockStatic(AgentPropertiesFileHandler.class)) {
+            
Mockito.when(AgentPropertiesFileHandler.getPropertyValue(Mockito.eq(AgentProperties.HOST_TAGS)))
+                    .thenReturn(" ");
+
+            List<String> hostTagsList = 
libvirtComputingResourceSpy.getHostTags();
+            Assert.assertEquals(0, hostTagsList.size());
+            Assert.assertEquals("", StringUtils.join(hostTagsList, ","));
+        }
+    }
 }
diff --git a/server/src/main/java/com/cloud/api/ApiDBUtils.java 
b/server/src/main/java/com/cloud/api/ApiDBUtils.java
index 46af53d68bf..a30abada404 100644
--- a/server/src/main/java/com/cloud/api/ApiDBUtils.java
+++ b/server/src/main/java/com/cloud/api/ApiDBUtils.java
@@ -103,7 +103,6 @@ import com.cloud.api.query.dao.DiskOfferingJoinDao;
 import com.cloud.api.query.dao.DomainJoinDao;
 import com.cloud.api.query.dao.DomainRouterJoinDao;
 import com.cloud.api.query.dao.HostJoinDao;
-import com.cloud.api.query.dao.HostTagDao;
 import com.cloud.api.query.dao.ImageStoreJoinDao;
 import com.cloud.api.query.dao.InstanceGroupJoinDao;
 import com.cloud.api.query.dao.NetworkOfferingJoinDao;
@@ -129,7 +128,6 @@ import com.cloud.api.query.vo.DomainJoinVO;
 import com.cloud.api.query.vo.DomainRouterJoinVO;
 import com.cloud.api.query.vo.EventJoinVO;
 import com.cloud.api.query.vo.HostJoinVO;
-import com.cloud.api.query.vo.HostTagVO;
 import com.cloud.api.query.vo.ImageStoreJoinVO;
 import com.cloud.api.query.vo.InstanceGroupJoinVO;
 import com.cloud.api.query.vo.NetworkOfferingJoinVO;
@@ -183,9 +181,11 @@ import com.cloud.gpu.dao.VGPUTypesDao;
 import com.cloud.ha.HighAvailabilityManager;
 import com.cloud.host.Host;
 import com.cloud.host.HostStats;
+import com.cloud.host.HostTagVO;
 import com.cloud.host.HostVO;
 import com.cloud.host.dao.HostDao;
 import com.cloud.host.dao.HostDetailsDao;
+import com.cloud.host.dao.HostTagsDao;
 import com.cloud.hypervisor.Hypervisor.HypervisorType;
 import com.cloud.network.IpAddress;
 import com.cloud.network.Network;
@@ -452,7 +452,7 @@ public class ApiDBUtils {
     static VolumeJoinDao s_volJoinDao;
     static StoragePoolJoinDao s_poolJoinDao;
     static StoragePoolTagsDao s_tagDao;
-    static HostTagDao s_hostTagDao;
+    static HostTagsDao s_hostTagDao;
     static ImageStoreJoinDao s_imageStoreJoinDao;
     static AccountJoinDao s_accountJoinDao;
     static AsyncJobJoinDao s_jobJoinDao;
@@ -675,7 +675,7 @@ public class ApiDBUtils {
     @Inject
     private StoragePoolTagsDao tagDao;
     @Inject
-    private HostTagDao hosttagDao;
+    private HostTagsDao hosttagDao;
     @Inject
     private ImageStoreJoinDao imageStoreJoinDao;
     @Inject
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 1b8a39900f2..8b61cdfc3e4 100644
--- a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java
+++ b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java
@@ -172,7 +172,6 @@ import com.cloud.api.query.dao.DiskOfferingJoinDao;
 import com.cloud.api.query.dao.DomainJoinDao;
 import com.cloud.api.query.dao.DomainRouterJoinDao;
 import com.cloud.api.query.dao.HostJoinDao;
-import com.cloud.api.query.dao.HostTagDao;
 import com.cloud.api.query.dao.ImageStoreJoinDao;
 import com.cloud.api.query.dao.InstanceGroupJoinDao;
 import com.cloud.api.query.dao.ManagementServerJoinDao;
@@ -197,7 +196,6 @@ import com.cloud.api.query.vo.DomainJoinVO;
 import com.cloud.api.query.vo.DomainRouterJoinVO;
 import com.cloud.api.query.vo.EventJoinVO;
 import com.cloud.api.query.vo.HostJoinVO;
-import com.cloud.api.query.vo.HostTagVO;
 import com.cloud.api.query.vo.ImageStoreJoinVO;
 import com.cloud.api.query.vo.InstanceGroupJoinVO;
 import com.cloud.api.query.vo.ManagementServerJoinVO;
@@ -229,8 +227,10 @@ import com.cloud.exception.InvalidParameterValueException;
 import com.cloud.exception.PermissionDeniedException;
 import com.cloud.ha.HighAvailabilityManager;
 import com.cloud.host.Host;
+import com.cloud.host.HostTagVO;
 import com.cloud.host.HostVO;
 import com.cloud.host.dao.HostDao;
+import com.cloud.host.dao.HostTagsDao;
 import com.cloud.hypervisor.Hypervisor;
 import com.cloud.hypervisor.Hypervisor.HypervisorType;
 import com.cloud.network.PublicIpQuarantine;
@@ -426,7 +426,7 @@ public class QueryManagerImpl extends 
MutualExclusiveIdsManagerBase implements Q
     private StoragePoolTagsDao _storageTagDao;
 
     @Inject
-    private HostTagDao _hostTagDao;
+    private HostTagsDao _hostTagDao;
 
     @Inject
     private ImageStoreJoinDao _imageStoreJoinDao;
@@ -2268,10 +2268,10 @@ public class QueryManagerImpl extends 
MutualExclusiveIdsManagerBase implements Q
         if (haHosts != null && haTag != null && !haTag.isEmpty()) {
             SearchBuilder<HostTagVO> hostTagSearchBuilder = 
_hostTagDao.createSearchBuilder();
             if ((Boolean)haHosts) {
-                hostTagSearchBuilder.and("tag", 
hostTagSearchBuilder.entity().getName(), SearchCriteria.Op.EQ);
+                hostTagSearchBuilder.and("tag", 
hostTagSearchBuilder.entity().getTag(), SearchCriteria.Op.EQ);
             } else {
-                hostTagSearchBuilder.and().op("tag", 
hostTagSearchBuilder.entity().getName(), Op.NEQ);
-                hostTagSearchBuilder.or("tagNull", 
hostTagSearchBuilder.entity().getName(), Op.NULL);
+                hostTagSearchBuilder.and().op("tag", 
hostTagSearchBuilder.entity().getTag(), Op.NEQ);
+                hostTagSearchBuilder.or("tagNull", 
hostTagSearchBuilder.entity().getTag(), Op.NULL);
                 hostTagSearchBuilder.cp();
             }
             hostSearchBuilder.join("hostTagSearch", hostTagSearchBuilder, 
hostSearchBuilder.entity().getId(), hostTagSearchBuilder.entity().getHostId(), 
JoinBuilder.JoinType.LEFT);
diff --git a/server/src/main/java/com/cloud/api/query/ViewResponseHelper.java 
b/server/src/main/java/com/cloud/api/query/ViewResponseHelper.java
index d22850b93f5..0c70839765b 100644
--- a/server/src/main/java/com/cloud/api/query/ViewResponseHelper.java
+++ b/server/src/main/java/com/cloud/api/query/ViewResponseHelper.java
@@ -74,7 +74,6 @@ import com.cloud.api.query.vo.DomainJoinVO;
 import com.cloud.api.query.vo.DomainRouterJoinVO;
 import com.cloud.api.query.vo.EventJoinVO;
 import com.cloud.api.query.vo.HostJoinVO;
-import com.cloud.api.query.vo.HostTagVO;
 import com.cloud.api.query.vo.ImageStoreJoinVO;
 import com.cloud.api.query.vo.InstanceGroupJoinVO;
 import com.cloud.api.query.vo.ProjectAccountJoinVO;
@@ -91,6 +90,7 @@ import com.cloud.api.query.vo.UserVmJoinVO;
 import com.cloud.api.query.vo.VolumeJoinVO;
 import com.cloud.configuration.Resource;
 import com.cloud.domain.Domain;
+import com.cloud.host.HostTagVO;
 import com.cloud.storage.Storage.ImageFormat;
 import com.cloud.storage.StoragePoolTagVO;
 import com.cloud.storage.VolumeStats;
diff --git a/server/src/main/java/com/cloud/api/query/dao/HostJoinDaoImpl.java 
b/server/src/main/java/com/cloud/api/query/dao/HostJoinDaoImpl.java
index f67c6d75994..49505821fd8 100644
--- a/server/src/main/java/com/cloud/api/query/dao/HostJoinDaoImpl.java
+++ b/server/src/main/java/com/cloud/api/query/dao/HostJoinDaoImpl.java
@@ -205,6 +205,8 @@ public class HostJoinDaoImpl extends 
GenericDaoBase<HostJoinVO, Long> implements
                 hostResponse.setHostTags(hostTags);
                 hostResponse.setIsTagARule(host.getIsTagARule());
                 hostResponse.setHaHost(containsHostHATag(hostTags));
+                hostResponse.setExplicitHostTags(host.getExplicitTag());
+                hostResponse.setImplicitHostTags(host.getImplicitTag());
 
                 hostResponse.setHypervisorVersion(host.getHypervisorVersion());
 
@@ -349,6 +351,7 @@ public class HostJoinDaoImpl extends 
GenericDaoBase<HostJoinVO, Long> implements
                 String hostTags = host.getTag();
                 hostResponse.setHostTags(hostTags);
                 hostResponse.setHaHost(containsHostHATag(hostTags));
+                hostResponse.setImplicitHostTags(host.getImplicitTag());
 
                 hostResponse.setHypervisorVersion(host.getHypervisorVersion());
 
diff --git a/server/src/main/java/com/cloud/api/query/dao/HostTagDao.java 
b/server/src/main/java/com/cloud/api/query/dao/HostTagDao.java
deleted file mode 100644
index ab43e71221c..00000000000
--- a/server/src/main/java/com/cloud/api/query/dao/HostTagDao.java
+++ /dev/null
@@ -1,30 +0,0 @@
-// 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 com.cloud.api.query.dao;
-
-import java.util.List;
-
-import org.apache.cloudstack.api.response.HostTagResponse;
-
-import com.cloud.api.query.vo.HostTagVO;
-import com.cloud.utils.db.GenericDao;
-
-public interface HostTagDao extends GenericDao<HostTagVO, Long> {
-    HostTagResponse newHostTagResponse(HostTagVO hostTag);
-
-    List<HostTagVO> searchByIds(Long... hostTagIds);
-}
diff --git a/server/src/main/java/com/cloud/api/query/dao/HostTagDaoImpl.java 
b/server/src/main/java/com/cloud/api/query/dao/HostTagDaoImpl.java
deleted file mode 100644
index d2a34bf5e58..00000000000
--- a/server/src/main/java/com/cloud/api/query/dao/HostTagDaoImpl.java
+++ /dev/null
@@ -1,122 +0,0 @@
-// 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 com.cloud.api.query.dao;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import javax.inject.Inject;
-
-import org.apache.cloudstack.api.response.HostTagResponse;
-import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
-import org.springframework.stereotype.Component;
-
-import com.cloud.api.query.vo.HostTagVO;
-import com.cloud.utils.db.GenericDaoBase;
-import com.cloud.utils.db.SearchBuilder;
-import com.cloud.utils.db.SearchCriteria;
-
-@Component
-public class HostTagDaoImpl extends GenericDaoBase<HostTagVO, Long> implements 
HostTagDao {
-
-    @Inject
-    private ConfigurationDao _configDao;
-
-    private final SearchBuilder<HostTagVO> stSearch;
-    private final SearchBuilder<HostTagVO> stIdSearch;
-
-    protected HostTagDaoImpl() {
-        stSearch = createSearchBuilder();
-
-        stSearch.and("idIN", stSearch.entity().getId(), SearchCriteria.Op.IN);
-        stSearch.done();
-
-        stIdSearch = createSearchBuilder();
-
-        stIdSearch.and("id", stIdSearch.entity().getId(), 
SearchCriteria.Op.EQ);
-        stIdSearch.done();
-
-        _count = "select count(distinct id) from host_tags WHERE ";
-    }
-
-    @Override
-    public HostTagResponse newHostTagResponse(HostTagVO tag) {
-        HostTagResponse tagResponse = new HostTagResponse();
-
-        tagResponse.setName(tag.getName());
-        tagResponse.setHostId(tag.getHostId());
-
-        tagResponse.setObjectName("hosttag");
-
-        return tagResponse;
-    }
-
-    @Override
-    public List<HostTagVO> searchByIds(Long... stIds) {
-        String batchCfg = _configDao.getValue("detail.batch.query.size");
-
-        final int detailsBatchSize = batchCfg != null ? 
Integer.parseInt(batchCfg) : 2000;
-
-        // query details by batches
-        List<HostTagVO> uvList = new ArrayList<HostTagVO>();
-        int curr_index = 0;
-
-        if (stIds.length > detailsBatchSize) {
-            while ((curr_index + detailsBatchSize) <= stIds.length) {
-                Long[] ids = new Long[detailsBatchSize];
-
-                for (int k = 0, j = curr_index; j < curr_index + 
detailsBatchSize; j++, k++) {
-                    ids[k] = stIds[j];
-                }
-
-                SearchCriteria<HostTagVO> sc = stSearch.create();
-
-                sc.setParameters("idIN", (Object[])ids);
-
-                List<HostTagVO> vms = searchIncludingRemoved(sc, null, null, 
false);
-
-                if (vms != null) {
-                    uvList.addAll(vms);
-                }
-
-                curr_index += detailsBatchSize;
-            }
-        }
-
-        if (curr_index < stIds.length) {
-            int batch_size = (stIds.length - curr_index);
-            // set the ids value
-            Long[] ids = new Long[batch_size];
-
-            for (int k = 0, j = curr_index; j < curr_index + batch_size; j++, 
k++) {
-                ids[k] = stIds[j];
-            }
-
-            SearchCriteria<HostTagVO> sc = stSearch.create();
-
-            sc.setParameters("idIN", (Object[])ids);
-
-            List<HostTagVO> vms = searchIncludingRemoved(sc, null, null, 
false);
-
-            if (vms != null) {
-                uvList.addAll(vms);
-            }
-        }
-
-        return uvList;
-    }
-}
diff --git a/server/src/main/java/com/cloud/api/query/vo/HostJoinVO.java 
b/server/src/main/java/com/cloud/api/query/vo/HostJoinVO.java
index 40e844c95da..4c5fa20f822 100644
--- a/server/src/main/java/com/cloud/api/query/vo/HostJoinVO.java
+++ b/server/src/main/java/com/cloud/api/query/vo/HostJoinVO.java
@@ -174,6 +174,12 @@ public class HostJoinVO extends BaseViewVO implements 
InternalIdentity, Identity
     @Column(name = "tag")
     private String tag;
 
+    @Column(name = "explicit_tag")
+    private String explicitTag;
+
+    @Column(name = "implicit_tag")
+    private String implicitTag;
+
     @Column(name = "is_tag_a_rule")
     private Boolean isTagARule;
 
@@ -393,6 +399,14 @@ public class HostJoinVO extends BaseViewVO implements 
InternalIdentity, Identity
         return tag;
     }
 
+    public String getExplicitTag() {
+        return explicitTag;
+    }
+
+    public String getImplicitTag() {
+        return implicitTag;
+    }
+
     public Boolean getIsTagARule() {
         return isTagARule;
     }
diff --git a/server/src/main/java/com/cloud/api/query/vo/HostTagVO.java 
b/server/src/main/java/com/cloud/api/query/vo/HostTagVO.java
deleted file mode 100644
index 0a279e5c490..00000000000
--- a/server/src/main/java/com/cloud/api/query/vo/HostTagVO.java
+++ /dev/null
@@ -1,61 +0,0 @@
-// 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 com.cloud.api.query.vo;
-
-import javax.persistence.Column;
-import javax.persistence.Entity;
-import javax.persistence.Id;
-import javax.persistence.Table;
-
-import org.apache.cloudstack.api.InternalIdentity;
-
-/**
- * Storage Tags DB view.
- *
- */
-@Entity
-@Table(name = "host_tags")
-public class HostTagVO extends BaseViewVO implements InternalIdentity {
-    private static final long serialVersionUID = 1L;
-
-    @Id
-    @Column(name = "id")
-    private long id;
-
-    @Column(name = "tag")
-    private String name;
-
-    @Column(name = "host_id")
-    long hostId;
-
-    @Override
-    public long getId() {
-        return id;
-    }
-
-    public String getName() {
-        return name;
-    }
-
-    public long getHostId() {
-        return hostId;
-    }
-
-    public void setHostId(long hostId) {
-        this.hostId = hostId;
-    }
-}
diff --git a/server/src/main/java/com/cloud/resource/ResourceManagerImpl.java 
b/server/src/main/java/com/cloud/resource/ResourceManagerImpl.java
index 6c5433c851a..d102470fe08 100755
--- a/server/src/main/java/com/cloud/resource/ResourceManagerImpl.java
+++ b/server/src/main/java/com/cloud/resource/ResourceManagerImpl.java
@@ -38,9 +38,9 @@ import javax.inject.Inject;
 import javax.naming.ConfigurationException;
 
 import com.cloud.alert.AlertManager;
-import com.cloud.host.HostTagVO;
 import com.cloud.exception.StorageConflictException;
 import com.cloud.exception.StorageUnavailableException;
+import com.cloud.host.HostTagVO;
 import com.cloud.storage.Volume;
 import com.cloud.storage.VolumeVO;
 import com.cloud.storage.dao.VolumeDao;
@@ -2334,22 +2334,6 @@ public class ResourceManagerImpl extends ManagerBase 
implements ResourceManager,
             }
         }
 
-        if (startup instanceof StartupRoutingCommand) {
-            final StartupRoutingCommand ssCmd = (StartupRoutingCommand)startup;
-            final List<String> implicitHostTags = ssCmd.getHostTags();
-            if (!implicitHostTags.isEmpty()) {
-                if (hostTags == null) {
-                    hostTags = 
_hostTagsDao.getHostTags(host.getId()).parallelStream().map(HostTagVO::getTag).collect(Collectors.toList());
-                }
-                if (hostTags != null) {
-                    implicitHostTags.removeAll(hostTags);
-                    hostTags.addAll(implicitHostTags);
-                } else {
-                    hostTags = implicitHostTags;
-                }
-            }
-        }
-
         host.setDataCenterId(dc.getId());
         host.setPodId(podId);
         host.setClusterId(clusterId);
@@ -2392,6 +2376,7 @@ public class ResourceManagerImpl extends ManagerBase 
implements ResourceManager,
 
         if (startup instanceof StartupRoutingCommand) {
             final StartupRoutingCommand ssCmd = (StartupRoutingCommand)startup;
+            _hostTagsDao.updateImplicitTags(host.getId(), ssCmd.getHostTags());
 
             updateSupportsClonedVolumes(host, 
ssCmd.getSupportsClonedVolumes());
         }
diff --git a/test/integration/smoke/test_host_tags.py 
b/test/integration/smoke/test_host_tags.py
new file mode 100644
index 00000000000..b6bfe79148d
--- /dev/null
+++ b/test/integration/smoke/test_host_tags.py
@@ -0,0 +1,160 @@
+# 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.
+""" Tests for importVolume and unmanageVolume APIs
+"""
+# Import Local Modules
+from marvin.cloudstackAPI import updateHost
+from marvin.cloudstackTestCase import cloudstackTestCase, unittest
+from marvin.lib.base import Host
+from marvin.lib.utils import is_server_ssh_ready, wait_until
+
+# Import System modules
+from nose.plugins.attrib import attr
+
+import logging
+
+class TestHostTags(cloudstackTestCase):
+
+    @classmethod
+    def setUpClass(cls):
+        testClient = super(TestHostTags, cls).getClsTestClient()
+        cls.apiclient = testClient.getApiClient()
+        cls.hypervisor = testClient.getHypervisorInfo()
+        if cls.testClient.getHypervisorInfo().lower() != "kvm":
+            raise unittest.SkipTest("This is only available for KVM")
+
+        cls.hostConfig = 
cls.config.__dict__["zones"][0].__dict__["pods"][0].__dict__["clusters"][0].__dict__["hosts"][0].__dict__
+
+        hosts = Host.list(
+            cls.apiclient,
+            type = "Routing",
+            hypervisor = cls.hypervisor
+        )
+        if isinstance(hosts, list) and len(hosts) > 0:
+            cls.host = hosts[0]
+        else:
+            raise unittest.SkipTest("No available host for this test")
+
+        cls.logger = logging.getLogger("TestHostTags")
+        cls.stream_handler = logging.StreamHandler()
+        cls.logger.setLevel(logging.DEBUG)
+        cls.logger.addHandler(cls.stream_handler)
+
+    @classmethod
+    def tearDownClass(cls):
+        cls.update_host_tags_via_api(cls.host.hosttags)
+        
cls.update_implicit_host_tags_via_agent_properties(cls.host.implicithosttags)
+
+    @classmethod
+    def update_host_tags_via_api(cls, hosttags):
+        cmd = updateHost.updateHostCmd()
+        cmd.id = cls.host.id
+        cmd.hosttags = hosttags
+        cls.apiclient.updateHost(cmd)
+
+    @classmethod
+    def update_implicit_host_tags_via_agent_properties(cls, implicithosttags):
+        ssh_client = is_server_ssh_ready(
+            cls.host.ipaddress,
+            22,
+            cls.hostConfig["username"],
+            cls.hostConfig["password"],
+        )
+        if implicithosttags:
+            command = "sed -i '/host.tags=/d' 
/etc/cloudstack/agent/agent.properties \
+                && echo 'host.tags=%s' >> 
/etc/cloudstack/agent/agent.properties \
+                && systemctl restart cloudstack-agent" % implicithosttags
+        else:
+            command = "sed -i '/host.tags=/d' 
/etc/cloudstack/agent/agent.properties \
+                && systemctl restart cloudstack-agent"
+
+        ssh_client.execute(command)
+
+    def wait_until_host_is_up_and_verify_hosttags(self, explicithosttags, 
implicithosttags, interval=3, retries=20):
+        def check_host_state():
+            hosts = Host.list(
+                self.apiclient,
+                id=self.host.id
+            )
+            if isinstance(hosts, list) and len(hosts) > 0:
+                host = hosts[0]
+                if host.state == "Up":
+                    self.logger.debug("Host %s is in Up state" % host.name)
+                    self.logger.debug("Host explicithosttags is %s, implicit 
hosttags is %s" % (host.explicithosttags, host.implicithosttags))
+                    if explicithosttags:
+                        self.assertEquals(explicithosttags, 
host.explicithosttags)
+                    else:
+                        self.assertIsNone(host.explicithosttags)
+                    if implicithosttags:
+                        self.assertEquals(implicithosttags, 
host.implicithosttags)
+                    else:
+                        self.assertIsNone(host.implicithosttags)
+                    return True, None
+                else:
+                    self.logger.debug("Waiting for host %s to be Up state, 
current state is %s" % (host.name, host.state))
+            return False, None
+
+        done, _ = wait_until(interval, retries, check_host_state)
+        if not done:
+            raise Exception("Failed to wait for host %s to be Up" % 
self.host.name)
+        return True
+
+    @attr(tags=['advanced', 'basic', 'sg'], required_hardware=False)
+    def test_01_host_tags(self):
+        """Test implicit/explicit host tags
+        """
+
+        # update explicit host tags to "s1,s2"
+        explicithosttags="s1,s2"
+        implicithosttags=self.host.implicithosttags
+        self.update_host_tags_via_api(explicithosttags)
+        self.wait_until_host_is_up_and_verify_hosttags(explicithosttags, 
implicithosttags)
+
+        # update implicit host tags to "d1,d2"
+        implicithosttags="d1,d2"
+        self.update_implicit_host_tags_via_agent_properties(implicithosttags)
+        self.wait_until_host_is_up_and_verify_hosttags(explicithosttags, 
implicithosttags)
+
+        # update explicit host tags to "s3,s4"
+        explicithosttags="s3,s4"
+        self.update_host_tags_via_api(explicithosttags)
+        self.wait_until_host_is_up_and_verify_hosttags(explicithosttags, 
implicithosttags)
+
+        # update implicit host tags to "d3,d4"
+        implicithosttags="d3,d4"
+        self.update_implicit_host_tags_via_agent_properties(implicithosttags)
+        self.wait_until_host_is_up_and_verify_hosttags(explicithosttags, 
implicithosttags)
+
+        # update hosttags to ""
+        explicithosttags=""
+        self.update_host_tags_via_api(explicithosttags)
+        self.wait_until_host_is_up_and_verify_hosttags(explicithosttags, 
implicithosttags)
+
+        # update implicit host tags to ""
+        implicithosttags=""
+        self.update_implicit_host_tags_via_agent_properties(implicithosttags)
+        self.wait_until_host_is_up_and_verify_hosttags(explicithosttags, 
implicithosttags)
+
+        # update explicit host tags to "s1,s2"
+        explicithosttags="s1,s2"
+        self.update_host_tags_via_api(explicithosttags)
+        self.wait_until_host_is_up_and_verify_hosttags(explicithosttags, 
implicithosttags)
+
+        # update implicit host tags to "d1,d2"
+        implicithosttags="d1,d2"
+        self.update_implicit_host_tags_via_agent_properties(implicithosttags)
+        self.wait_until_host_is_up_and_verify_hosttags(explicithosttags, 
implicithosttags)
diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json
index 57543086181..25928e6c1fd 100644
--- a/ui/public/locales/en.json
+++ b/ui/public/locales/en.json
@@ -198,6 +198,7 @@
 "label.action.unmanage.virtualmachine": "Unmanage Instance",
 "label.action.unmanage.volume": "Unmanage Volume",
 "label.action.unmanage.volumes": "Unmanage Volumes",
+"label.action.update.host": "Update host",
 "label.action.update.offering.access": "Update offering access",
 "label.action.update.resource.count": "Update resource count",
 "label.action.value": "Action/Value",
@@ -1006,6 +1007,12 @@
 "label.hostnamelabel": "Host name",
 "label.hosts": "Hosts",
 "label.hosttags": "Host tags",
+"label.hosttags.explicit": "API-defined Host tags",
+"label.hosttags.explicit.abbr": "api-defined",
+"label.hosttags.explicit.description": "The host tags defined by CloudStack 
APIs",
+"label.hosttags.implicit": "Agent-defined Host tags",
+"label.hosttags.implicit.abbr": "agent-defined",
+"label.hosttags.implicit.description": "The host tags defined by CloudStack 
Agent",
 "label.hourly": "Hourly",
 "label.hypervisor": "Hypervisor",
 "label.hypervisor.capabilities": "Hypervisor capabilities",
diff --git a/ui/src/config/section/infra/hosts.js 
b/ui/src/config/section/infra/hosts.js
index 329b77fe2d7..88e20aa43fc 100644
--- a/ui/src/config/section/infra/hosts.js
+++ b/ui/src/config/section/infra/hosts.js
@@ -74,12 +74,8 @@ export default {
       icon: 'edit-outlined',
       label: 'label.edit',
       dataView: true,
-      args: ['name', 'hosttags', 'istagarule', 'oscategoryid'],
-      mapping: {
-        oscategoryid: {
-          api: 'listOsCategories'
-        }
-      }
+      popup: true,
+      component: shallowRef(defineAsyncComponent(() => 
import('@/views/infra/HostUpdate')))
     },
     {
       api: 'provisionCertificate',
diff --git a/ui/src/views/infra/HostInfo.vue b/ui/src/views/infra/HostInfo.vue
index 1d0b47eba95..0ad6b86b740 100644
--- a/ui/src/views/infra/HostInfo.vue
+++ b/ui/src/views/infra/HostInfo.vue
@@ -51,8 +51,14 @@
       <a-list-item v-if="host.hosttags">
         <div>
           <strong>{{ $t('label.hosttags') }}</strong>
-          <div>
-            {{ host.hosttags }}
+          <div v-for="hosttag in host.allhosttags" :key="hosttag.tag">
+            {{ hosttag.tag }}
+            <span v-if="hosttag.isexplicit">
+              <a-tag color="blue">{{ $t('label.hosttags.explicit.abbr') 
}}</a-tag>
+            </span>
+            <span v-if="hosttag.isimplicit">
+              <a-tag color="orange">{{ $t('label.hosttags.implicit.abbr') 
}}</a-tag>
+            </span>
           </div>
         </div>
       </a-list-item>
@@ -158,6 +164,22 @@ export default {
       this.fetchLoading = true
       api('listHosts', { id: this.resource.id }).then(json => {
         this.host = json.listhostsresponse.host[0]
+        const hosttags = this.host.hosttags?.split(',') || []
+        const explicithosttags = this.host.explicithosttags?.split(',') || []
+        const implicithosttags = this.host.implicithosttags?.split(',') || []
+        const allHostTags = []
+        for (const hosttag of hosttags) {
+          var isexplicit = false
+          var isimplicit = false
+          if (explicithosttags.includes(hosttag)) {
+            isexplicit = true
+          }
+          if (implicithosttags.includes(hosttag)) {
+            isimplicit = true
+          }
+          allHostTags.push({ tag: hosttag, isexplicit: isexplicit, isimplicit: 
isimplicit })
+        }
+        this.host.allhosttags = allHostTags
       }).catch(error => {
         this.$notifyError(error)
       }).finally(() => {
diff --git a/ui/src/views/infra/HostUpdate.vue 
b/ui/src/views/infra/HostUpdate.vue
new file mode 100644
index 00000000000..aeb2a3c92a6
--- /dev/null
+++ b/ui/src/views/infra/HostUpdate.vue
@@ -0,0 +1,183 @@
+// 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.
+
+<template>
+  <a-spin :spinning="loading">
+    <a-form
+      class="form-layout"
+      layout="vertical"
+      :ref="formRef"
+      :model="form"
+      :rules="rules"
+      v-ctrl-enter="handleSubmit"
+      @finish="handleSubmit">
+      <a-form-item name="name" ref="name">
+        <template #label>
+          <tooltip-label :title="$t('label.name')" 
:tooltip="apiParams.name.description"/>
+        </template>
+        <a-input
+          v-model:value="form.name"
+          v-focus="true" />
+      </a-form-item>
+      <a-form-item name="hosttags" ref="hosttags">
+        <template #label>
+          <tooltip-label :title="$t('label.hosttags')" 
:tooltip="$t('label.hosttags.explicit.description')"/>
+        </template>
+        <a-input v-model:value="form.hosttags" />
+      </a-form-item>
+      <a-form-item name="istagarule" ref="istagarule">
+        <template #label>
+          <tooltip-label :title="$t('label.istagarule')" 
:tooltip="apiParams.istagarule.description"/>
+        </template>
+        <a-switch v-model:checked="form.istagarule" />
+      </a-form-item>
+      <a-form-item name="oscategoryid" ref="oscategoryid">
+        <template #label>
+          <tooltip-label :title="$t('label.oscategoryid')" 
:tooltip="apiParams.oscategoryid.description"/>
+        </template>
+        <a-select
+          showSearch
+          optionFilterProp="label"
+          :filterOption="(input, option) => {
+            return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
+          }"
+          :loading="osCategories.loading"
+          v-model:value="form.oscategoryid">
+          <a-select-option v-for="(osCategory) in osCategories.opts" 
:key="osCategory.id" :label="osCategory.name">
+            {{ osCategory.name }}
+          </a-select-option>
+        </a-select>
+      </a-form-item>
+
+      <div :span="24" class="action-button">
+        <a-button :loading="loading" @click="onCloseAction">{{ 
$t('label.cancel') }}</a-button>
+        <a-button :loading="loading" ref="submit" type="primary" 
@click="handleSubmit">{{ $t('label.ok') }}</a-button>
+      </div>
+    </a-form>
+  </a-spin>
+</template>
+
+<script>
+import { ref, reactive, toRaw } from 'vue'
+import { api } from '@/api'
+import TooltipLabel from '@/components/widgets/TooltipLabel'
+
+export default {
+  name: 'EditVM',
+  components: {
+    TooltipLabel
+  },
+  props: {
+    action: {
+      type: Object,
+      required: true
+    },
+    resource: {
+      type: Object,
+      required: true
+    }
+  },
+  data () {
+    return {
+      loading: false,
+      osCategories: {
+        loading: false,
+        opts: []
+      }
+    }
+  },
+  beforeCreate () {
+    this.apiParams = this.$getApiParams('updateHost')
+  },
+  created () {
+    this.initForm()
+    this.fetchOsCategories()
+  },
+  methods: {
+    initForm () {
+      this.formRef = ref()
+      this.form = reactive({
+        name: this.resource.name,
+        hosttags: this.resource.explicithosttags,
+        istagarule: this.resource.istagarule,
+        oscategoryid: this.resource.oscategoryid
+      })
+      this.rules = reactive({})
+    },
+    fetchOsCategories () {
+      this.osCategories.loading = true
+      this.osCategories.opts = []
+      api('listOsCategories').then(json => {
+        this.osCategories.opts = json.listoscategoriesresponse.oscategory || []
+      }).catch(error => {
+        this.$notifyError(error)
+      }).finally(() => {
+        this.osCategories.loading = false
+      })
+    },
+    handleSubmit () {
+      this.formRef.value.validate().then(() => {
+        const values = toRaw(this.form)
+        const params = {}
+        params.id = this.resource.id
+        params.name = values.name
+        params.hosttags = values.hosttags
+        params.oscategoryid = values.oscategoryid
+        if (values.istagarule !== undefined) {
+          params.istagarule = values.istagarule
+        }
+        this.loading = true
+
+        api('updateHost', params).then(json => {
+          this.$message.success({
+            content: `${this.$t('label.action.update.host')} - ${values.name}`,
+            duration: 2
+          })
+          this.$emit('refresh-data')
+          this.onCloseAction()
+        }).catch(error => {
+          this.$notifyError(error)
+        }).finally(() => { this.loading = false })
+      }).catch(error => {
+        this.formRef.value.scrollToField(error.errorFields[0].name)
+      })
+    },
+    onCloseAction () {
+      this.$emit('close-action')
+    }
+  }
+}
+</script>
+
+<style scoped lang="less">
+.form-layout {
+  width: 80vw;
+
+  @media (min-width: 600px) {
+    width: 450px;
+  }
+
+  .action-button {
+    text-align: right;
+    margin-top: 20px;
+
+    button {
+      margin-right: 5px;
+    }
+  }
+}
+</style>

Reply via email to