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>