From 5433e775e53ad3709db6286cb7b0a78703c823a6 Mon Sep 17 00:00:00 2001 From: Wei Zhou Date: Thu, 30 May 2024 13:51:13 +0200 Subject: [PATCH] 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 + .../agent/properties/AgentProperties.java | 7 + .../apache/cloudstack/api/ApiConstants.java | 1 + .../response/HostForMigrationResponse.java | 16 ++ .../cloudstack/api/response/HostResponse.java | 24 +++ .../api/response/HostTagResponse.java | 13 ++ .../agent/api/StartupRoutingCommand.java | 4 + .../main/java/com/cloud/host/HostTagVO.java | 10 + .../java/com/cloud/host/dao/HostTagsDao.java | 8 + .../com/cloud/host/dao/HostTagsDaoImpl.java | 125 ++++++++++++ ...spring-engine-schema-core-daos-context.xml | 1 - .../META-INF/db/schema-41900to42000.sql | 3 + .../META-INF/db/views/cloud.host_view.sql | 8 +- .../resource/LibvirtComputingResource.java | 14 ++ .../LibvirtComputingResourceTest.java | 36 ++++ .../main/java/com/cloud/api/ApiDBUtils.java | 8 +- .../com/cloud/api/query/QueryManagerImpl.java | 12 +- .../cloud/api/query/ViewResponseHelper.java | 2 +- .../cloud/api/query/dao/HostJoinDaoImpl.java | 3 + .../com/cloud/api/query/dao/HostTagDao.java | 30 --- .../cloud/api/query/dao/HostTagDaoImpl.java | 122 ------------ .../com/cloud/api/query/vo/HostJoinVO.java | 14 ++ .../com/cloud/api/query/vo/HostTagVO.java | 61 ------ .../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(-) delete mode 100644 server/src/main/java/com/cloud/api/query/dao/HostTagDao.java delete mode 100644 server/src/main/java/com/cloud/api/query/dao/HostTagDaoImpl.java delete mode 100644 server/src/main/java/com/cloud/api/query/vo/HostTagVO.java create mode 100644 test/integration/smoke/test_host_tags.py create mode 100644 ui/src/views/infra/HostUpdate.vue 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 KEYSTORE_PASSPHRASE = new Property<>(KeyStoreUtils.KS_PASSPHRASE_PROPERTY, null, String.class); + /** + * Implicit host tags + * Data type: String.
+ * Default value: null + */ + public static final Property HOST_TAGS = new Property<>("host.tags", null, String.class); + public static class Property { 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 hostTags) { + this.hostTags = hostTags; + } + public HashMap> 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 { @@ -35,6 +36,13 @@ public interface HostTagsDao extends GenericDao { void deleteTags(long hostId); + boolean updateImplicitTags(long hostId, List hostTags); + + List getExplicitHostTags(long hostId); + List findHostRuleTags(); + HostTagResponse newHostTagResponse(HostTagVO hostTag); + + List 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 implements HostTagsDao, Configurable { protected final SearchBuilder HostSearch; protected final GenericSearchBuilder DistinctImplictTagsSearch; + private final SearchBuilder stSearch; + private final SearchBuilder tagIdsearch; + private final SearchBuilder 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 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 implements txn.commit(); } + @Override + public boolean updateImplicitTags(long hostId, List hostTags) { + TransactionLegacy txn = TransactionLegacy.currentTxn(); + txn.start(); + SearchCriteria 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 getExplicitHostTags(long hostId) { + SearchCriteria sc = ImplicitTagsSearch.create(); + sc.setParameters("hostId", hostId); + sc.setParameters("isImplicit", false); + + return search(sc, null); + } + @Override public List findHostRuleTags() { SearchCriteria sc = HostSearch.create(); @@ -89,6 +145,7 @@ public class HostTagsDaoImpl extends GenericDaoBase implements txn.start(); SearchCriteria 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 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 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 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 sc = stSearch.create(); + + sc.setParameters("idIN", (Object[])ids); + + List 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 sc = stSearch.create(); + + sc.setParameters("idIN", (Object[])ids); + + List 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 @@ - 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 getHostTags() { + List 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. *
    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 ignored = Mockito.mockStatic(AgentPropertiesFileHandler.class)) { + Mockito.when(AgentPropertiesFileHandler.getPropertyValue(Mockito.eq(AgentProperties.HOST_TAGS))) + .thenReturn("aa,bb,cc,dd"); + + List 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 ignored = Mockito.mockStatic(AgentPropertiesFileHandler.class)) { + Mockito.when(AgentPropertiesFileHandler.getPropertyValue(Mockito.eq(AgentProperties.HOST_TAGS))) + .thenReturn(" aa, bb , cc , dd "); + + List 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 ignored = Mockito.mockStatic(AgentPropertiesFileHandler.class)) { + Mockito.when(AgentPropertiesFileHandler.getPropertyValue(Mockito.eq(AgentProperties.HOST_TAGS))) + .thenReturn(" "); + + List 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 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 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 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 { - HostTagResponse newHostTagResponse(HostTagVO hostTag); - - List 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 implements HostTagDao { - - @Inject - private ConfigurationDao _configDao; - - private final SearchBuilder stSearch; - private final SearchBuilder 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 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 uvList = new ArrayList(); - 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 sc = stSearch.create(); - - sc.setParameters("idIN", (Object[])ids); - - List 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 sc = stSearch.create(); - - sc.setParameters("idIN", (Object[])ids); - - List 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 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 @@
    {{ $t('label.hosttags') }} -
    - {{ host.hosttags }} +
    + {{ hosttag.tag }} + + {{ $t('label.hosttags.explicit.abbr') }} + + + {{ $t('label.hosttags.implicit.abbr') }} +
    @@ -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. + + + + + +