From fc78948e6f1cdf6e3458e18ec51fb5ac37f62fd7 Mon Sep 17 00:00:00 2001 From: Fabricio Duarte Date: Thu, 7 May 2026 13:12:05 -0300 Subject: [PATCH 1/3] Guest OS rules --- api/src/main/java/com/cloud/host/Host.java | 2 + .../apache/cloudstack/api/ApiConstants.java | 1 + .../api/command/admin/host/UpdateHostCmd.java | 11 ++- .../cloudstack/api/response/HostResponse.java | 12 +++ .../src/main/java/com/cloud/host/HostVO.java | 4 +- .../main/java/com/cloud/host/dao/HostDao.java | 7 ++ .../java/com/cloud/host/dao/HostDaoImpl.java | 37 ++++++- .../META-INF/db/views/cloud.host_view.sql | 1 + .../cloudstack/quota/QuotaManagerImpl.java | 2 +- .../response/QuotaResponseBuilderImpl.java | 2 +- .../manager/allocator/impl/BaseAllocator.java | 25 +++++ .../allocator/impl/FirstFitAllocator.java | 7 +- .../allocator/impl/RandomAllocator.java | 3 +- .../main/java/com/cloud/api/ApiDBUtils.java | 12 +++ .../cloud/api/query/dao/HostJoinDaoImpl.java | 1 + .../api/query/dao/StoragePoolJoinDaoImpl.java | 5 +- .../com/cloud/api/query/vo/HostJoinVO.java | 7 ++ .../ConfigurationManagerImpl.java | 8 +- .../deploy/DeploymentPlanningManagerImpl.java | 52 ++++++++-- .../cloud/resource/ResourceManagerImpl.java | 98 +++++++++++++------ .../cloud/storage/VolumeApiServiceImpl.java | 5 +- .../heuristics/HeuristicRuleHelper.java | 2 +- .../allocator/impl/FirstFitAllocatorTest.java | 14 +-- ui/public/locales/pt_BR.json | 3 + ui/src/views/infra/HostUpdate.vue | 32 +++++- ...RuleHelper.java => GenericRuleHelper.java} | 60 +++++++----- .../utils/jsinterpreter/JsInterpreter.java | 6 +- 27 files changed, 325 insertions(+), 94 deletions(-) rename utils/src/main/java/org/apache/cloudstack/utils/jsinterpreter/{TagAsRuleHelper.java => GenericRuleHelper.java} (52%) diff --git a/api/src/main/java/com/cloud/host/Host.java b/api/src/main/java/com/cloud/host/Host.java index b52348201516..8b14cfd3a390 100644 --- a/api/src/main/java/com/cloud/host/Host.java +++ b/api/src/main/java/com/cloud/host/Host.java @@ -63,6 +63,8 @@ public static String[] toStrings(Host.Type... types) { String HOST_OVFTOOL_VERSION = "host.ovftool.version"; String HOST_VIRTV2V_VERSION = "host.virtv2v.version"; String HOST_SSH_PORT = "host.ssh.port"; + String GUEST_OS_CATEGORY_ID = "guest.os.category.id"; + String GUEST_OS_RULE = "guest.os.rule"; int DEFAULT_SSH_PORT = 22; 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 4d4ead277e5d..556abd360d44 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -442,6 +442,7 @@ public class ApiConstants { public static final String MAX_VGPU_PER_PHYSICAL_GPU = "maxvgpuperphysicalgpu"; public static final String GUEST_OS_LIST = "guestoslist"; public static final String GUEST_OS_COUNT = "guestoscount"; + public static final String GUEST_OS_RULE = "guestosrule"; public static final String OS_MAPPING_CHECK_ENABLED = "osmappingcheckenabled"; public static final String OUTOFBANDMANAGEMENT_POWERSTATE = "outofbandmanagementpowerstate"; public static final String OUTOFBANDMANAGEMENT_ENABLED = "outofbandmanagementenabled"; diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/host/UpdateHostCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/host/UpdateHostCmd.java index c085abd42c76..8e66b2256707 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/host/UpdateHostCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/host/UpdateHostCmd.java @@ -49,9 +49,14 @@ public class UpdateHostCmd extends BaseCmd { @Parameter(name = ApiConstants.OS_CATEGORY_ID, type = CommandType.UUID, entityType = GuestOSCategoryResponse.class, - description = "The ID of OS category to update the host with") + description = "the ID of OS category used to prioritize VMs with mathing OS category during the allocation process. " + + "It cannot be used alongside the 'guestosrule' parameter.") private Long osCategoryId; + @Parameter(name = ApiConstants.GUEST_OS_RULE, type = CommandType.STRING, description = "the guest OS rule written in JavaScript to match with the OS of the VM." + + "It cannot be used alongside the 'oscategoryid' parameter.") + private String guestOsRule; + @Parameter(name = ApiConstants.ALLOCATION_STATE, type = CommandType.STRING, description = "Change resource state of host, valid values are [Enable, Disable]. Operation may failed if host in states not allowing Enable/Disable") @@ -96,6 +101,10 @@ public Long getOsCategoryId() { return osCategoryId; } + public String getGuestOsRule() { + return guestOsRule; + } + public String getAllocationState() { return allocationState; } 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 5d085d1bee05..f8e1af8d5b76 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 @@ -63,6 +63,10 @@ public class HostResponse extends BaseResponseWithAnnotations { @Param(description = "The OS category name of the host") private String osCategoryName; + @SerializedName(ApiConstants.GUEST_OS_RULE) + @Param(description = "the guest OS rule") + private String guestOsRule; + @SerializedName(ApiConstants.IP_ADDRESS) @Param(description = "The IP address of the host") private String ipAddress; @@ -999,4 +1003,12 @@ public void setExtensionName(String extensionName) { public String getExtensionName() { return extensionName; } + + public String getGuestOsRule() { + return guestOsRule; + } + + public void setGuestOsRule(String guestOsRule) { + this.guestOsRule = guestOsRule; + } } diff --git a/engine/schema/src/main/java/com/cloud/host/HostVO.java b/engine/schema/src/main/java/com/cloud/host/HostVO.java index d51b4eca0577..dc10c539a026 100644 --- a/engine/schema/src/main/java/com/cloud/host/HostVO.java +++ b/engine/schema/src/main/java/com/cloud/host/HostVO.java @@ -45,7 +45,7 @@ import com.cloud.cpu.CPU; import org.apache.cloudstack.util.CPUArchConverter; import org.apache.cloudstack.util.HypervisorTypeConverter; -import org.apache.cloudstack.utils.jsinterpreter.TagAsRuleHelper; +import org.apache.cloudstack.utils.jsinterpreter.GenericRuleHelper; import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang.BooleanUtils; @@ -856,7 +856,7 @@ public boolean checkHostServiceOfferingTags(ServiceOffering serviceOffering) { } if (BooleanUtils.isTrue(this.getIsTagARule())) { - return TagAsRuleHelper.interpretTagAsRule(this.getHostTags().get(0), serviceOffering.getHostTag(), HostTagsDao.hostTagRuleExecutionTimeout.value()); + return GenericRuleHelper.interpretTagAsRule(this.getHostTags().get(0), serviceOffering.getHostTag(), HostTagsDao.hostTagRuleExecutionTimeout.value()); } if (StringUtils.isEmpty(serviceOffering.getHostTag())) { diff --git a/engine/schema/src/main/java/com/cloud/host/dao/HostDao.java b/engine/schema/src/main/java/com/cloud/host/dao/HostDao.java index 5f5b2affee08..5c0d04fb2be8 100644 --- a/engine/schema/src/main/java/com/cloud/host/dao/HostDao.java +++ b/engine/schema/src/main/java/com/cloud/host/dao/HostDao.java @@ -32,12 +32,17 @@ import com.cloud.utils.db.GenericDao; import com.cloud.utils.fsm.StateDao; import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo; +import org.apache.cloudstack.framework.config.ConfigKey; /** * Data Access Object for server * */ public interface HostDao extends GenericDao, StateDao { + + ConfigKey guestOsRuleExecutionTimeout = new ConfigKey<>("Advanced", Long.class, "guest.os.rule.execution.timeout", "3000", "The maximum runtime, in milliseconds, " + + "to execute a guest OS rule; if it is reached, a timeout will happen.", true); + long countBy(long clusterId, ResourceState... states); Integer countAllByType(final Host.Type type); @@ -220,6 +225,8 @@ int countHostsByMsResourceStateTypeAndHypervisorType(long msId, List findHostsWithTagRuleThatMatchComputeOfferingTags(String computeOfferingTags); + List findHostsWithGuestOsRulesThatDidNotMatchOsOfGuestVm(String templateGuestOSName); + List findClustersThatMatchHostTagRule(String computeOfferingTags); List listSsvmHostsWithPendingMigrateJobsOrderedByJobCount(); diff --git a/engine/schema/src/main/java/com/cloud/host/dao/HostDaoImpl.java b/engine/schema/src/main/java/com/cloud/host/dao/HostDaoImpl.java index 99c9a979c3bf..cd4423dfa269 100644 --- a/engine/schema/src/main/java/com/cloud/host/dao/HostDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/host/dao/HostDaoImpl.java @@ -37,7 +37,9 @@ import com.cloud.vm.VirtualMachine; import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo; -import org.apache.cloudstack.utils.jsinterpreter.TagAsRuleHelper; +import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.framework.config.Configurable; +import org.apache.cloudstack.utils.jsinterpreter.GenericRuleHelper; import org.apache.commons.collections.CollectionUtils; import com.cloud.agent.api.VgpuTypesInfo; @@ -84,7 +86,7 @@ @DB @TableGenerator(name = "host_req_sq", table = "op_host", pkColumnName = "id", valueColumnName = "sequence", allocationSize = 1) -public class HostDaoImpl extends GenericDaoBase implements HostDao { //FIXME: , ExternalIdDao { +public class HostDaoImpl extends GenericDaoBase implements HostDao, Configurable { private static final String LIST_HOST_IDS_BY_HOST_TAGS = "SELECT filtered.host_id, COUNT(filtered.tag) AS tag_count " + "FROM (SELECT host_id, tag, is_tag_a_rule FROM host_tags GROUP BY host_id,tag,is_tag_a_rule) AS filtered " @@ -1520,11 +1522,13 @@ private List findHostIdsByHostTags(String hostTags){ } } + @Override public List findHostsWithTagRuleThatMatchComputeOfferingTags(String computeOfferingTags) { List hostTagVOList = _hostTagsDao.findHostRuleTags(); List result = new ArrayList<>(); for (HostTagVO rule: hostTagVOList) { - if (TagAsRuleHelper.interpretTagAsRule(rule.getTag(), computeOfferingTags, HostTagsDao.hostTagRuleExecutionTimeout.value())) { + if (GenericRuleHelper.interpretTagAsRule(rule.getTag(), computeOfferingTags, HostTagsDao.hostTagRuleExecutionTimeout.value(), + HostTagsDao.hostTagRuleExecutionTimeout.key())) { result.add(findById(rule.getHostId())); } } @@ -1532,6 +1536,23 @@ public List findHostsWithTagRuleThatMatchComputeOfferingTags(String comp return result; } + @Override + public List findHostsWithGuestOsRulesThatDidNotMatchOsOfGuestVm(String templateGuestOSName) { + List hostIdsWithGuestOsRule = _detailsDao.findByName(Host.GUEST_OS_RULE); + List hostsWithIncompatibleRules = new ArrayList<>(); + for (DetailVO guestOsRule : hostIdsWithGuestOsRule) { + if (!GenericRuleHelper.interpretGuestOsRule(guestOsRule.getValue(), templateGuestOSName, HostDao.guestOsRuleExecutionTimeout.value(), + HostDao.guestOsRuleExecutionTimeout.key())) { + logger.trace("The guest OS rule [{}] of the host with ID [{}] is incompatible with the OS of the VM.", + guestOsRule.getHostId(), guestOsRule.getValue()); + hostsWithIncompatibleRules.add(findById(guestOsRule.getHostId())); + } + } + logger.trace("The hosts with the following IDs [{}] are incompatible with the VM considering their guest OS rule.", + hostsWithIncompatibleRules); + return hostsWithIncompatibleRules; + } + public List findClustersThatMatchHostTagRule(String computeOfferingTags) { Set result = new HashSet<>(); List hosts = findHostsWithTagRuleThatMatchComputeOfferingTags(computeOfferingTags); @@ -1984,4 +2005,14 @@ public List listDistinctStorageAccessGroups(String name, String keyword) return customSearch(sc, null); } + + @Override + public ConfigKey[] getConfigKeys() { + return new ConfigKey[] {guestOsRuleExecutionTimeout}; + } + + @Override + public String getConfigComponentName() { + return HostDaoImpl.class.getSimpleName(); + } } 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 d9f4e2671595..255ce866c427 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 @@ -64,6 +64,7 @@ SELECT guest_os_category.id guest_os_category_id, guest_os_category.uuid guest_os_category_uuid, guest_os_category.name guest_os_category_name, + (SELECT `value` FROM `cloud`.`host_details` `hd` WHERE `hd`.`host_id` = `cloud`.`host`.`id` AND `hd`.`name` = 'guest.os.rule') AS `guest_os_rule`, mem_caps.used_capacity memory_used_capacity, mem_caps.reserved_capacity memory_reserved_capacity, cpu_caps.used_capacity cpu_used_capacity, diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/QuotaManagerImpl.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/QuotaManagerImpl.java index 816144aa2f16..87e45dfd4fd6 100644 --- a/framework/quota/src/main/java/org/apache/cloudstack/quota/QuotaManagerImpl.java +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/QuotaManagerImpl.java @@ -312,7 +312,7 @@ protected List createQuotaUsagesAccordingToQuotaTariffs(AccountVO List> pairsUsageAndQuotaUsage = new ArrayList<>(); - try (JsInterpreter jsInterpreter = new JsInterpreter(QuotaConfig.QuotaActivationRuleTimeout.value())) { + try (JsInterpreter jsInterpreter = new JsInterpreter(QuotaConfig.QuotaActivationRuleTimeout.value(), QuotaConfig.QuotaActivationRuleTimeout.key())) { for (UsageVO usageRecord : usageRecords) { int usageType = usageRecord.getUsageType(); diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java index c919bb5887c1..e3229f97e448 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java @@ -1244,7 +1244,7 @@ public QuotaValidateActivationRuleResponse validateActivationRule(QuotaValidateA addAllPresetVariables(PresetVariables.class, quotaType, usageTypeVariablesAndDescriptions, null); List usageTypeVariables = usageTypeVariablesAndDescriptions.stream().map(Pair::first).collect(Collectors.toList()); - try (JsInterpreter jsInterpreter = new JsInterpreter(QuotaConfig.QuotaActivationRuleTimeout.value())) { + try (JsInterpreter jsInterpreter = new JsInterpreter(QuotaConfig.QuotaActivationRuleTimeout.value(), QuotaConfig.QuotaActivationRuleTimeout.key())) { Map newVariables = injectUsageTypeVariables(jsInterpreter, usageTypeVariables); String scriptToExecute = jsInterpreterHelper.replaceScriptVariables(activationRule, newVariables); jsInterpreter.executeScript(String.format("new Function(\"%s\")", scriptToExecute.replaceAll("\n", ""))); diff --git a/server/src/main/java/com/cloud/agent/manager/allocator/impl/BaseAllocator.java b/server/src/main/java/com/cloud/agent/manager/allocator/impl/BaseAllocator.java index 58fcc62cdc31..ad5726e0a010 100644 --- a/server/src/main/java/com/cloud/agent/manager/allocator/impl/BaseAllocator.java +++ b/server/src/main/java/com/cloud/agent/manager/allocator/impl/BaseAllocator.java @@ -17,11 +17,13 @@ package com.cloud.agent.manager.allocator.impl; import com.cloud.agent.manager.allocator.HostAllocator; +import com.cloud.api.ApiDBUtils; import com.cloud.capacity.CapacityManager; import com.cloud.host.Host; import com.cloud.host.HostVO; import com.cloud.host.dao.HostDao; import com.cloud.offering.ServiceOffering; +import com.cloud.storage.VMTemplateVO; import com.cloud.utils.Pair; import com.cloud.utils.component.AdapterBase; import org.apache.commons.collections.CollectionUtils; @@ -69,6 +71,29 @@ protected void addHostsBasedOnTagRules(String hostTagOnOffering, List cl clusterHosts.addAll(hostsWithTagRules); } + protected void filterHostsBasedOnGuestOsRules(VMTemplateVO vmTemplate, List clusterHosts) { + if (clusterHosts.isEmpty()) { + logger.info("Will not filter hosts based on guest OS as there is no available hosts left to verify."); + return; + } + + String templateGuestOSName = ApiDBUtils.getTemplateGuestOSName(vmTemplate); + List incompatibleHosts = hostDao.findHostsWithGuestOsRulesThatDidNotMatchOsOfGuestVm(templateGuestOSName); + + if (incompatibleHosts.isEmpty()) { + logger.info("No incompatible hosts found with guest OS rules matching the VM guest OS [{}].", templateGuestOSName); + return; + } + + logger.info("Found incompatible hosts {} with guest OS rules that did not match the VM guest OS [{}]. They will be removed from the suitable hosts list.", + incompatibleHosts, templateGuestOSName); + clusterHosts.removeAll(incompatibleHosts); + + if (clusterHosts.isEmpty()) { + logger.info("After filtering by guest OS rules, no compatible hosts were found for VM with OS [{}].", templateGuestOSName); + } + } + /** * Adds hosts with enough CPU capability and enough CPU capacity to the suitable hosts list. */ diff --git a/server/src/main/java/com/cloud/agent/manager/allocator/impl/FirstFitAllocator.java b/server/src/main/java/com/cloud/agent/manager/allocator/impl/FirstFitAllocator.java index 8b4ca318bf78..4bc34d8a5c60 100644 --- a/server/src/main/java/com/cloud/agent/manager/allocator/impl/FirstFitAllocator.java +++ b/server/src/main/java/com/cloud/agent/manager/allocator/impl/FirstFitAllocator.java @@ -123,7 +123,7 @@ public List allocateTo(VirtualMachineProfile vmProfile, DeploymentPlan pla String hostTagOnTemplate = template.getTemplateTag(); String paramAsStringToLog = String.format("zone [%s], pod [%s], cluster [%s]", dcId, podId, clusterId); - List suitableHosts = retrieveHosts(vmProfile, type, (List) hosts, clusterId, podId, dcId, hostTagOnOffering, hostTagOnTemplate); + List suitableHosts = retrieveHosts(vmProfile, type, (List) hosts, template, clusterId, podId, dcId, hostTagOnOffering, hostTagOnTemplate); if (suitableHosts.isEmpty()) { logger.info("No suitable host found for VM [{}] in {}.", vmProfile, paramAsStringToLog); @@ -137,8 +137,8 @@ public List allocateTo(VirtualMachineProfile vmProfile, DeploymentPlan pla return allocateTo(vmProfile, plan, offering, template, avoid, suitableHosts, returnUpTo, considerReservedCapacity, account); } - protected List retrieveHosts(VirtualMachineProfile vmProfile, Type type, List hostsToFilter, Long clusterId, Long podId, long dcId, String hostTagOnOffering, - String hostTagOnTemplate) { + protected List retrieveHosts(VirtualMachineProfile vmProfile, Type type, List hostsToFilter, VMTemplateVO template, + Long clusterId, Long podId, long dcId, String hostTagOnOffering, String hostTagOnTemplate) { String haVmTag = (String) vmProfile.getParameter(VirtualMachineProfile.Param.HaTag); List clusterHosts; @@ -159,6 +159,7 @@ protected List retrieveHosts(VirtualMachineProfile vmProfile, Type type, filterHostsWithUefiEnabled(type, vmProfile, clusterId, podId, dcId, clusterHosts); addHostsBasedOnTagRules(hostTagOnOffering, clusterHosts); + filterHostsBasedOnGuestOsRules(template, clusterHosts); return clusterHosts; diff --git a/server/src/main/java/com/cloud/agent/manager/allocator/impl/RandomAllocator.java b/server/src/main/java/com/cloud/agent/manager/allocator/impl/RandomAllocator.java index 96b4255b876d..92fdaf7faf0e 100644 --- a/server/src/main/java/com/cloud/agent/manager/allocator/impl/RandomAllocator.java +++ b/server/src/main/java/com/cloud/agent/manager/allocator/impl/RandomAllocator.java @@ -42,7 +42,7 @@ public class RandomAllocator extends BaseAllocator { private ResourceManager _resourceMgr; protected List findSuitableHosts(VirtualMachineProfile vmProfile, DeploymentPlan plan, Type type, ExcludeList avoid, List hosts, int returnUpTo, - boolean considerReservedCapacity) { + boolean considerReservedCapacity) { if (type == Host.Type.Storage) { return null; } @@ -115,6 +115,7 @@ protected List retrieveHosts(Type type, List hosts, VMTemplateVO } addHostsBasedOnTagRules(offeringHostTag, availableHosts); + filterHostsBasedOnGuestOsRules(template, availableHosts); return availableHosts; } diff --git a/server/src/main/java/com/cloud/api/ApiDBUtils.java b/server/src/main/java/com/cloud/api/ApiDBUtils.java index 1d00e9ec16ba..d8b3c155ae8c 100644 --- a/server/src/main/java/com/cloud/api/ApiDBUtils.java +++ b/server/src/main/java/com/cloud/api/ApiDBUtils.java @@ -31,6 +31,7 @@ import javax.inject.Inject; import com.cloud.cpu.CPU; +import com.cloud.storage.GuestOSVO; import org.apache.cloudstack.acl.Role; import org.apache.cloudstack.acl.RoleService; import org.apache.cloudstack.affinity.AffinityGroup; @@ -2345,4 +2346,15 @@ public static SharedFSJoinVO newSharedFSView(SharedFS sharedFS) { public static List listZoneClustersArchs(long zoneId) { return s_clusterDao.getClustersArchsByZone(zoneId); } + + public static String getTemplateGuestOSName(VMTemplateVO template) { + long guestOSId = template.getGuestOSId(); + GuestOSVO guestOS = s_guestOSDao.findById(guestOSId); + + if (guestOS == null) { + return null; + } + + return guestOS.getDisplayName(); + } } 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 e7265a7e3b9a..005e0ab493a0 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 @@ -221,6 +221,7 @@ private void setNewHostResponseBase(HostJoinVO host, EnumSet detail hostResponse.setHaHost(containsHostHATag(hostTags)); hostResponse.setExplicitHostTags(host.getExplicitTag()); hostResponse.setImplicitHostTags(host.getImplicitTag()); + hostResponse.setGuestOsRule(host.getGuestOsRule()); hostResponse.setHypervisorVersion(host.getHypervisorVersion()); if (host.getArch() != null) { diff --git a/server/src/main/java/com/cloud/api/query/dao/StoragePoolJoinDaoImpl.java b/server/src/main/java/com/cloud/api/query/dao/StoragePoolJoinDaoImpl.java index 8bfce47b1204..5665baaa3c3b 100644 --- a/server/src/main/java/com/cloud/api/query/dao/StoragePoolJoinDaoImpl.java +++ b/server/src/main/java/com/cloud/api/query/dao/StoragePoolJoinDaoImpl.java @@ -34,7 +34,7 @@ import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailVO; import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailsDao; import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; -import org.apache.cloudstack.utils.jsinterpreter.TagAsRuleHelper; +import org.apache.cloudstack.utils.jsinterpreter.GenericRuleHelper; import org.springframework.stereotype.Component; import com.cloud.api.ApiDBUtils; @@ -398,7 +398,8 @@ public List findStoragePoolByScopeAndRuleTags(Long datacenterId, String injectableTag = injectableTagsBuilder.toString(); for (StoragePoolJoinVO storagePoolJoinVO : storagePools) { - if (TagAsRuleHelper.interpretTagAsRule(storagePoolJoinVO.getTag(), injectableTag, VolumeApiServiceImpl.storageTagRuleExecutionTimeout.value())) { + if (GenericRuleHelper.interpretTagAsRule(storagePoolJoinVO.getTag(), injectableTag, VolumeApiServiceImpl.storageTagRuleExecutionTimeout.value(), + VolumeApiServiceImpl.storageTagRuleExecutionTimeout.key())) { StoragePoolVO storagePoolVO = storagePoolDao.findById(storagePoolJoinVO.getId()); if (storagePoolVO != null) { filteredPools.add(storagePoolVO); 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 83cfcc8375f3..24ab5291a42a 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 @@ -173,6 +173,9 @@ public class HostJoinVO extends BaseViewVO implements InternalIdentity, Identity @Column(name = "guest_os_category_name") private String osCategoryName; + @Column(name = "guest_os_rule") + private String guestOsRule; + @Column(name = "tag") private String tag; @@ -373,6 +376,10 @@ public String getOsCategoryName() { return osCategoryName; } + public String getGuestOsRule() { + return guestOsRule; + } + public Long getJobId() { return jobId; } diff --git a/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java b/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java index 6da5dda967d0..6f15d1f0b83e 100644 --- a/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java +++ b/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java @@ -143,7 +143,7 @@ import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailsDao; import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; import org.apache.cloudstack.userdata.UserDataManager; -import org.apache.cloudstack.utils.jsinterpreter.TagAsRuleHelper; +import org.apache.cloudstack.utils.jsinterpreter.GenericRuleHelper; import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; import org.apache.cloudstack.vm.UnmanagedVMsManager; import org.apache.cloudstack.vm.lease.VMLeaseManager; @@ -5083,7 +5083,8 @@ protected void updateOfferingTagsIfIsNotNull(String tags, DiskOfferingVO diskOff if ((CollectionUtils.isNotEmpty(tagsAsString) && tagsAsString.containsAll(listOfTags)) || (tagsOnPool.size() == 1 && tagsOnPool.get(0).isTagARule() && - TagAsRuleHelper.interpretTagAsRule(tagsOnPool.get(0).getTag(), tags, VolumeApiServiceImpl.storageTagRuleExecutionTimeout.value()))) { + GenericRuleHelper.interpretTagAsRule(tagsOnPool.get(0).getTag(), tags, VolumeApiServiceImpl.storageTagRuleExecutionTimeout.value(), + VolumeApiServiceImpl.storageTagRuleExecutionTimeout.key()))) { continue; } @@ -5126,7 +5127,8 @@ protected void updateServiceOfferingHostTagsIfNotNull(String hostTags, ServiceOf if ((CollectionUtils.isNotEmpty(tagsAsString) && tagsAsString.containsAll(listOfHostTags)) || (tagsOnHost.size() == 1 && tagsOnHost.get(0).getIsTagARule() && - TagAsRuleHelper.interpretTagAsRule(tagsOnHost.get(0).getTag(), hostTags, HostTagsDao.hostTagRuleExecutionTimeout.value()))) { + GenericRuleHelper.interpretTagAsRule(tagsOnHost.get(0).getTag(), hostTags, HostTagsDao.hostTagRuleExecutionTimeout.value(), + HostTagsDao.hostTagRuleExecutionTimeout.key()))) { continue; } diff --git a/server/src/main/java/com/cloud/deploy/DeploymentPlanningManagerImpl.java b/server/src/main/java/com/cloud/deploy/DeploymentPlanningManagerImpl.java index 7fb4a97e149b..f194c5786906 100644 --- a/server/src/main/java/com/cloud/deploy/DeploymentPlanningManagerImpl.java +++ b/server/src/main/java/com/cloud/deploy/DeploymentPlanningManagerImpl.java @@ -35,6 +35,7 @@ import javax.inject.Inject; import javax.naming.ConfigurationException; +import com.cloud.api.ApiDBUtils; import com.cloud.gpu.dao.VgpuProfileDao; import com.cloud.resource.ResourceState; import org.apache.cloudstack.affinity.AffinityGroupDomainMapVO; @@ -805,18 +806,49 @@ protected boolean checkVmProfileAndHost(final VirtualMachineProfile vmProfile, f return false; } } + + DetailVO guestOsRule = _hostDetailsDao.findDetail(host.getId(), HostVO.GUEST_OS_RULE); + if (guestOsRule != null) { + return isHostGuestOsCompatible(vmProfile, host, guestOsRule); + } + + DetailVO guestOsCategoryId = _hostDetailsDao.findDetail(host.getId(), HostVO.GUEST_OS_CATEGORY_ID); + if (guestOsCategoryId != null) { + return isHostGuestOsCategoryCompatible(vmProfile, host, guestOsCategoryId); + } + + return true; + } + + private boolean isHostGuestOsCategoryCompatible(VirtualMachineProfile vmProfile, HostVO host, DetailVO guestOsCategoryId) { long guestOSId = vmProfile.getTemplate().getGuestOSId(); GuestOSVO guestOS = _guestOSDao.findById(guestOSId); - if (guestOS != null) { - long guestOSCategoryId = guestOS.getCategoryId(); - DetailVO hostDetail = _hostDetailsDao.findDetail(host.getId(), "guest.os.category.id"); - if (hostDetail != null) { - String guestOSCategoryIdString = hostDetail.getValue(); - if (String.valueOf(guestOSCategoryId) != guestOSCategoryIdString) { - logger.debug("The last host has different guest.os.category.id than guest os category of VM, skipping"); - return false; - } - } + + if (guestOS == null) { + return true; + } + + long guestOSCategoryId = guestOS.getCategoryId(); + String guestOSCategoryIdString = guestOsCategoryId.getValue(); + if (!String.valueOf(guestOSCategoryId).equals(guestOSCategoryIdString)) { + logger.debug("The last host has different `{}` than guest os category of VM, skipping", Host.GUEST_OS_CATEGORY_ID); + return false; + } + + return true; + } + + private boolean isHostGuestOsCompatible(VirtualMachineProfile vmProfile, HostVO host, DetailVO guestOsRule) { + VMTemplateVO template = (VMTemplateVO) vmProfile.getTemplate(); + String templateGuestOSName = ApiDBUtils.getTemplateGuestOSName(template); + + List incompatibleHosts = _hostDao.findHostsWithGuestOsRulesThatDidNotMatchOsOfGuestVm(templateGuestOSName); + + boolean isIncompatibleHost = incompatibleHosts.stream().noneMatch(incompatibleHost -> incompatibleHost.getId() == host.getId()); + + if (!isIncompatibleHost) { + logger.debug("The template [{}] is incompatible with the host [{}] due to the guest OS rule [{}].", template, host, guestOsRule); + return false; } return true; } diff --git a/server/src/main/java/com/cloud/resource/ResourceManagerImpl.java b/server/src/main/java/com/cloud/resource/ResourceManagerImpl.java index 0f6933664f93..8db55790fd8a 100755 --- a/server/src/main/java/com/cloud/resource/ResourceManagerImpl.java +++ b/server/src/main/java/com/cloud/resource/ResourceManagerImpl.java @@ -84,8 +84,8 @@ import org.apache.cloudstack.utils.identity.ManagementServerNode; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.collections.MapUtils; -import org.apache.commons.lang.ObjectUtils; import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.ObjectUtils; import org.jetbrains.annotations.NotNull; import org.springframework.stereotype.Component; @@ -2046,31 +2046,74 @@ private void updateHostName(HostVO host, String name) { _hostDao.update(host.getId(), host); } - private void updateHostGuestOSCategory(Long hostId, Long guestOSCategoryId) { - // Verify that the guest OS Category exists - if (!(guestOSCategoryId > 0) || _guestOSCategoryDao.findById(guestOSCategoryId) == null) { + /** + * Updates the guest OS details related to the host ({@code guest.os.rule} and {@code guest.os.category.id}); only one can be set at a time for a given host. + */ + private void updateGuestOsRelatedFields(Long hostId, Long cmdOsCategoryId, String cmdGuestOsRule) { + if (ObjectUtils.allNotNull(cmdOsCategoryId, cmdGuestOsRule)) { + throw new InvalidParameterValueException("Informing both guest OS category and guest OS rule is invalid; please, inform only one of them."); + } + + if (cmdOsCategoryId != null) { + updateHostCategoryOs(hostId, cmdOsCategoryId); + } else if (cmdGuestOsRule != null) { + updateHostGuestOsRule(hostId, cmdGuestOsRule); + } + } + + /** + * Associates/removes a guest OS rule to a host and removes any guest OS category associated with it. + */ + private void updateHostGuestOsRule(Long hostId, String cmdGuestOsRule) { + final DetailVO hostGuestOsRule = _hostDetailsDao.findDetail(hostId, Host.GUEST_OS_RULE); + + if (StringUtils.isNotBlank(cmdGuestOsRule)) { + createOrUpdateHostDetails(hostGuestOsRule, cmdGuestOsRule, Host.GUEST_OS_RULE, hostId); + } else if (hostGuestOsRule != null) { + _hostDetailsDao.remove(hostGuestOsRule.getId()); + } + removeHostDetails(hostId, Host.GUEST_OS_CATEGORY_ID); + } + + /** + * Removes a host details if it exists. + */ + private void removeHostDetails(Long hostId, String detailString) { + DetailVO detailVO = _hostDetailsDao.findDetail(hostId, detailString); + + if (detailVO != null) { + _hostDetailsDao.remove(detailVO.getId()); + } + } + + private void createOrUpdateHostDetails(DetailVO hostDetail, String detailValue, String detailName, Long hostId) { + if (hostDetail != null) { + hostDetail.setValue(detailValue); + _hostDetailsDao.update(hostDetail.getId(), hostDetail); + } else { + Map detail = new HashMap<>(); + detail.put(detailName, detailValue); + _hostDetailsDao.persist(hostId, detail); + } + } + + /** + * Associates/removes a valid guest OS category to a host and removes any guest OS rules associated with it. + */ + private void updateHostCategoryOs(Long hostId, Long cmdOsCategoryId) { + if (cmdOsCategoryId <= 0 || _guestOSCategoryDao.findById(cmdOsCategoryId) == null) { throw new InvalidParameterValueException("Please specify a valid guest OS category."); } - final GuestOSCategoryVO guestOSCategory = _guestOSCategoryDao.findById(guestOSCategoryId); - final DetailVO guestOSDetail = _hostDetailsDao.findDetail(hostId, "guest.os.category.id"); + GuestOSCategoryVO guestOSCategory = _guestOSCategoryDao.findById(cmdOsCategoryId); + DetailVO guestOSDetail = _hostDetailsDao.findDetail(hostId, Host.GUEST_OS_CATEGORY_ID); if (guestOSCategory != null && !GuestOSCategoryVO.CATEGORY_NONE.equalsIgnoreCase(guestOSCategory.getName())) { - // Create/Update an entry for guest.os.category.id - if (guestOSDetail != null) { - guestOSDetail.setValue(String.valueOf(guestOSCategory.getId())); - _hostDetailsDao.update(guestOSDetail.getId(), guestOSDetail); - } else { - final Map detail = new HashMap<>(); - detail.put("guest.os.category.id", String.valueOf(guestOSCategory.getId())); - _hostDetailsDao.persist(hostId, detail); - } - } else { - // Delete any existing entry for guest.os.category.id - if (guestOSDetail != null) { - _hostDetailsDao.remove(guestOSDetail.getId()); - } + createOrUpdateHostDetails(guestOSDetail, String.valueOf(guestOSCategory.getId()), Host.GUEST_OS_CATEGORY_ID, hostId); + } else if (guestOSDetail != null) { + _hostDetailsDao.remove(guestOSDetail.getId()); } + removeHostDetails(hostId, Host.GUEST_OS_RULE); } private void removeStorageAccessGroupsOnPodsInZone(long zoneId, List newStoragePoolTags, List tagsToDeleteOnZone) { @@ -2821,16 +2864,17 @@ private void updateHostTags(HostVO host, Long hostId, List hostTags, Boo @Override public Host updateHost(final UpdateHostCmd cmd) throws NoTransitionException { - return updateHost(cmd.getId(), cmd.getName(), cmd.getOsCategoryId(), + return updateHost(cmd.getId(), cmd.getName(), cmd.getOsCategoryId(), cmd.getGuestOsRule(), cmd.getAllocationState(), cmd.getUrl(), cmd.getHostTags(), cmd.getIsTagARule(), cmd.getAnnotation(), false, cmd.getExternalDetails(), cmd.isCleanupExternalDetails()); } - private Host updateHost(Long hostId, String name, Long guestOSCategoryId, String allocationState, + private Host updateHost(Long hostId, String name, Long guestOSCategoryId, String guestOsRule, String allocationState, String url, List hostTags, Boolean isTagARule, String annotation, boolean isUpdateFromHostHealthCheck, Map externalDetails, boolean cleanupExternalDetails) throws NoTransitionException { jsInterpreterHelper.ensureInterpreterEnabledIfParameterProvided(ApiConstants.IS_TAG_A_RULE, Boolean.TRUE.equals(isTagARule)); + jsInterpreterHelper.ensureInterpreterEnabledIfParameterProvided(ApiConstants.GUEST_OS_RULE, guestOsRule != null); // Verify that the host exists final HostVO host = _hostDao.findById(hostId); @@ -2847,9 +2891,7 @@ private Host updateHost(Long hostId, String name, Long guestOSCategoryId, String updateHostName(host, name); } - if (guestOSCategoryId != null) { - updateHostGuestOSCategory(hostId, guestOSCategoryId); - } + updateGuestOsRelatedFields(hostId, guestOSCategoryId, guestOsRule); if (hostTags != null) { updateHostTags(host, hostId, hostTags, isTagARule); @@ -2919,7 +2961,7 @@ private void sendAlertAndAnnotationForAutoEnableDisableKVMHostFeature(HostVO hos @Override public Host autoUpdateHostAllocationState(Long hostId, ResourceState.Event resourceEvent) throws NoTransitionException { - return updateHost(hostId, null, null, resourceEvent.toString(), null, null, null, null, true, null, false); + return updateHost(hostId, null, null, null, resourceEvent.toString(), null, null, null, null, true, null, false); } @Override @@ -3639,7 +3681,7 @@ private void checkIPConflicts(final HostPodVO pod, final DataCenterVO dc, final // If the server's private IP is the same as is public IP, this host has // a host-only private network. Don't check for conflicts with the // private IP address table. - if (!ObjectUtils.equals(serverPrivateIP, serverPublicIP)) { + if (!StringUtils.equals(serverPrivateIP, serverPublicIP)) { if (!_privateIPAddressDao.mark(dc.getId(), pod.getId(), serverPrivateIP)) { // If the server's private IP address is already in the // database, return false @@ -4284,7 +4326,7 @@ public Long getGuestOSCategoryId(final long hostId) { return null; } else { _hostDao.loadDetails(host); - final DetailVO detail = _hostDetailsDao.findDetail(hostId, "guest.os.category.id"); + final DetailVO detail = _hostDetailsDao.findDetail(hostId, Host.GUEST_OS_CATEGORY_ID); if (detail == null) { return null; } else { diff --git a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java index 6872d2070d66..0c4760d24cfe 100644 --- a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java +++ b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java @@ -112,7 +112,7 @@ import org.apache.cloudstack.storage.to.VolumeObjectTO; import org.apache.cloudstack.utils.identity.ManagementServerNode; import org.apache.cloudstack.utils.imagestore.ImageStoreUtil; -import org.apache.cloudstack.utils.jsinterpreter.TagAsRuleHelper; +import org.apache.cloudstack.utils.jsinterpreter.GenericRuleHelper; import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; import org.apache.cloudstack.utils.volume.VirtualMachineDiskInfo; import org.apache.commons.collections.CollectionUtils; @@ -3883,7 +3883,8 @@ public boolean doesStoragePoolSupportDiskOfferingTags(StoragePool destPool, Stri boolean result; if (storagePoolTags.second()) { - result = TagAsRuleHelper.interpretTagAsRule(storageTagsList.get(0), diskOfferingTags, storageTagRuleExecutionTimeout.value()); + result = GenericRuleHelper.interpretTagAsRule(storageTagsList.get(0), diskOfferingTags, storageTagRuleExecutionTimeout.value(), + storageTagRuleExecutionTimeout.key()); } else { result = CollectionUtils.isSubCollection(Arrays.asList(newDiskOfferingTagsAsStringArray), storageTagsList); } diff --git a/server/src/main/java/org/apache/cloudstack/storage/heuristics/HeuristicRuleHelper.java b/server/src/main/java/org/apache/cloudstack/storage/heuristics/HeuristicRuleHelper.java index beecf90d2b83..97c13f714474 100644 --- a/server/src/main/java/org/apache/cloudstack/storage/heuristics/HeuristicRuleHelper.java +++ b/server/src/main/java/org/apache/cloudstack/storage/heuristics/HeuristicRuleHelper.java @@ -257,7 +257,7 @@ protected Domain setDomainPresetVariable(long domainId) { * @return the {@link DataStore} returned by the script. */ public DataStore interpretHeuristicRule(String rule, HeuristicType heuristicType, Object obj, long zoneId) { - try (JsInterpreter jsInterpreter = new JsInterpreter(HEURISTICS_SCRIPT_TIMEOUT)) { + try (JsInterpreter jsInterpreter = new JsInterpreter(HEURISTICS_SCRIPT_TIMEOUT, StorageManager.HEURISTICS_SCRIPT_TIMEOUT.key())) { buildPresetVariables(jsInterpreter, heuristicType, zoneId, obj); Object scriptReturn = jsInterpreter.executeScript(rule); diff --git a/server/src/test/java/com/cloud/agent/manager/allocator/impl/FirstFitAllocatorTest.java b/server/src/test/java/com/cloud/agent/manager/allocator/impl/FirstFitAllocatorTest.java index 00cac5bbd52c..076e256eeaf6 100644 --- a/server/src/test/java/com/cloud/agent/manager/allocator/impl/FirstFitAllocatorTest.java +++ b/server/src/test/java/com/cloud/agent/manager/allocator/impl/FirstFitAllocatorTest.java @@ -193,7 +193,7 @@ public void allocateToTestSuitableHostsEmptyShouldReturnNull() { Mockito.doReturn(account).when(virtualMachineProfile).getOwner(); Mockito.doReturn(hostTag).when(serviceOffering).getHostTag(); Mockito.doReturn(templateTag).when(vmTemplateVO).getTemplateTag(); - Mockito.doReturn(emptyList).when(firstFitAllocatorSpy).retrieveHosts(Mockito.any(VirtualMachineProfile.class), Mockito.any(Host.Type.class), Mockito.nullable(List.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyString(), Mockito.anyString()); + Mockito.doReturn(emptyList).when(firstFitAllocatorSpy).retrieveHosts(Mockito.any(VirtualMachineProfile.class), Mockito.any(Host.Type.class), Mockito.nullable(List.class), Mockito.any(VMTemplateVO.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyString(), Mockito.anyString()); List suitableHosts = firstFitAllocatorSpy.allocateTo(virtualMachineProfile, deploymentPlan, type, excludeList, null, HostAllocator.RETURN_UPTO_ALL, considerReservedCapacity); Assert.assertNull(suitableHosts); @@ -208,7 +208,7 @@ public void allocateToTestSuitableHostsNotEmptyShouldCallAllocateToMethod() { Mockito.doReturn(account).when(virtualMachineProfile).getOwner(); Mockito.doReturn(hostTag).when(serviceOffering).getHostTag(); Mockito.doReturn(templateTag).when(vmTemplateVO).getTemplateTag(); - Mockito.doReturn(hosts).when(firstFitAllocatorSpy).retrieveHosts(Mockito.any(VirtualMachineProfile.class), Mockito.any(Host.Type.class), Mockito.nullable(List.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyString(), Mockito.anyString()); + Mockito.doReturn(hosts).when(firstFitAllocatorSpy).retrieveHosts(Mockito.any(VirtualMachineProfile.class), Mockito.any(Host.Type.class), Mockito.nullable(List.class), Mockito.any(VMTemplateVO.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyString(), Mockito.anyString()); Mockito.doReturn(hosts).when(firstFitAllocatorSpy).allocateTo(Mockito.any(VirtualMachineProfile.class), Mockito.any(DeploymentPlan.class), Mockito.any(ServiceOffering.class), Mockito.any(VMTemplateVO.class), Mockito.any(DeploymentPlanner.ExcludeList.class), Mockito.anyList(), Mockito.anyInt(), Mockito.anyBoolean(), Mockito.any(Account.class)); Mockito.doNothing().when(firstFitAllocatorSpy).addHostsToAvoidSet(Mockito.any(Host.Type.class), Mockito.any(DeploymentPlanner.ExcludeList.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyList()); List suitableHosts = firstFitAllocatorSpy.allocateTo(virtualMachineProfile, deploymentPlan, type, excludeList, null, HostAllocator.RETURN_UPTO_ALL, considerReservedCapacity); @@ -226,7 +226,7 @@ public void allocateToTestProvidedHostsNotNullShouldCallAddHostsToAvoidSetMethod Mockito.doReturn(account).when(virtualMachineProfile).getOwner(); Mockito.doReturn(hostTag).when(serviceOffering).getHostTag(); Mockito.doReturn(templateTag).when(vmTemplateVO).getTemplateTag(); - Mockito.doReturn(hosts).when(firstFitAllocatorSpy).retrieveHosts(Mockito.any(VirtualMachineProfile.class), Mockito.any(Host.Type.class), Mockito.nullable(List.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyString(), Mockito.anyString()); + Mockito.doReturn(hosts).when(firstFitAllocatorSpy).retrieveHosts(Mockito.any(VirtualMachineProfile.class), Mockito.any(Host.Type.class), Mockito.nullable(List.class), Mockito.any(VMTemplateVO.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyString(), Mockito.anyString()); Mockito.doReturn(hosts).when(firstFitAllocatorSpy).allocateTo(Mockito.any(VirtualMachineProfile.class), Mockito.any(DeploymentPlan.class), Mockito.any(ServiceOffering.class), Mockito.any(VMTemplateVO.class), Mockito.any(DeploymentPlanner.ExcludeList.class), Mockito.anyList(), Mockito.anyInt(), Mockito.anyBoolean(), Mockito.any(Account.class)); Mockito.doNothing().when(firstFitAllocatorSpy).addHostsToAvoidSet(Mockito.any(Host.Type.class), Mockito.any(DeploymentPlanner.ExcludeList.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyList()); firstFitAllocatorSpy.allocateTo(virtualMachineProfile, deploymentPlan, type, excludeList, null, HostAllocator.RETURN_UPTO_ALL, considerReservedCapacity); @@ -245,7 +245,7 @@ public void retrieveHostsTestHostsToFilterIsNullAndHaTagNotNullShouldReturnOnlyH Mockito.doReturn(hostsWithHaTag).when(hostDaoMock).listByHostTag(Mockito.any(Host.Type.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyString()); Mockito.doNothing().when(firstFitAllocatorSpy).filterHostsWithUefiEnabled(Mockito.any(Host.Type.class), Mockito.any(VirtualMachineProfile.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyList()); Mockito.doNothing().when(firstFitAllocatorSpy).addHostsBasedOnTagRules(Mockito.anyString(), Mockito.anyList()); - List resultHosts = firstFitAllocatorSpy.retrieveHosts(virtualMachineProfile, type, emptyList, clusterId, podId, dcId, hostTag, templateTag); + List resultHosts = firstFitAllocatorSpy.retrieveHosts(virtualMachineProfile, type, emptyList, vmTemplateVO, clusterId, podId, dcId, hostTag, templateTag); Assert.assertEquals(2, resultHosts.size()); Assert.assertEquals(host1, resultHosts.get(0)); @@ -262,7 +262,7 @@ public void retrieveHostsTestHostsToFilterIsNotNullAndHaTagNotNullShouldReturnOn Mockito.doReturn(hostsWithHaTag).when(hostDaoMock).listByHostTag(Mockito.any(Host.Type.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyString()); Mockito.doNothing().when(firstFitAllocatorSpy).filterHostsWithUefiEnabled(Mockito.any(Host.Type.class), Mockito.any(VirtualMachineProfile.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyList()); Mockito.doNothing().when(firstFitAllocatorSpy).addHostsBasedOnTagRules(Mockito.anyString(), Mockito.anyList()); - List resultHosts = firstFitAllocatorSpy.retrieveHosts(virtualMachineProfile, type, hostsToFilter, clusterId, podId, dcId, hostTag, templateTag); + List resultHosts = firstFitAllocatorSpy.retrieveHosts(virtualMachineProfile, type, hostsToFilter, vmTemplateVO, clusterId, podId, dcId, hostTag, templateTag); Assert.assertEquals(2, resultHosts.size()); Assert.assertEquals(host1, resultHosts.get(0)); @@ -278,7 +278,7 @@ public void retrieveHostsTestHostsToFilterIsNullAndNoHaTagAndNoHostTagShouldRetu Mockito.doReturn(upAndEnabledHostsWithNoHa).when(resourceManagerMock).listAllUpAndEnabledNonHAHosts(Mockito.any(Host.Type.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong()); Mockito.doNothing().when(firstFitAllocatorSpy).filterHostsWithUefiEnabled(Mockito.any(Host.Type.class), Mockito.any(VirtualMachineProfile.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyList()); Mockito.doNothing().when(firstFitAllocatorSpy).addHostsBasedOnTagRules(Mockito.nullable(String.class), Mockito.anyList()); - List resultHosts = firstFitAllocatorSpy.retrieveHosts(virtualMachineProfile, type, emptyList, clusterId, podId, dcId, null, null); + List resultHosts = firstFitAllocatorSpy.retrieveHosts(virtualMachineProfile, type, emptyList, vmTemplateVO, clusterId, podId, dcId, null, null); Assert.assertEquals(2, resultHosts.size()); Assert.assertEquals(host1, resultHosts.get(0)); @@ -293,7 +293,7 @@ public void retrieveHostsTestHostsToFilterIsNullAndNoHaTagWithHostTagShouldCallR Mockito.doNothing().when(firstFitAllocatorSpy).retainHostsMatchingServiceOfferingAndTemplateTags(Mockito.anyList(), Mockito.any(Host.Type.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyString(), Mockito.anyString()); Mockito.doNothing().when(firstFitAllocatorSpy).filterHostsWithUefiEnabled(Mockito.any(Host.Type.class), Mockito.any(VirtualMachineProfile.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyList()); Mockito.doNothing().when(firstFitAllocatorSpy).addHostsBasedOnTagRules(Mockito.anyString(), Mockito.anyList()); - firstFitAllocatorSpy.retrieveHosts(virtualMachineProfile, type, emptyList, clusterId, podId, dcId, hostTag, templateTag); + firstFitAllocatorSpy.retrieveHosts(virtualMachineProfile, type, emptyList, vmTemplateVO, clusterId, podId, dcId, hostTag, templateTag); Mockito.verify(firstFitAllocatorSpy, Mockito.times(1)).retainHostsMatchingServiceOfferingAndTemplateTags(Mockito.anyList(), Mockito.any(Host.Type.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyString(), Mockito.anyString()); } diff --git a/ui/public/locales/pt_BR.json b/ui/public/locales/pt_BR.json index d2910a3a2b98..a62f2667119b 100644 --- a/ui/public/locales/pt_BR.json +++ b/ui/public/locales/pt_BR.json @@ -159,6 +159,7 @@ "label.action.unmanage.instance": "Parar de gerenciar inst\u00e2ncia", "label.action.unmanage.instances": "Parar de gerenciar inst\u00e2ncias", "label.action.unmanage.virtualmachine": "Parar de gerenciar VM", +"label.action.update.host": "Atualizar host", "label.action.update.offering.access": "Atualizar acesso \u00e0 oferta", "label.action.update.resource.count": "Atualizar contagem de recursos", "label.action.vmsnapshot.create": "Criar snapshot de VM", @@ -752,6 +753,8 @@ "label.guestnetwork": "Rede guest", "label.guestnetworkid": "ID de rede", "label.guestnetworkname": "Nome da rede", +"label.guestosasrule": "SO guest como regra JS", +"label.guestosrule": "Regra SO guest", "label.gueststartip": "IP de in\u00edcio do guest", "label.guestvlanrange": "Intervalo(s) de VLAN", "label.guestvmcidr": "CIDR", diff --git a/ui/src/views/infra/HostUpdate.vue b/ui/src/views/infra/HostUpdate.vue index 1fee7a6846c0..10cf531e7705 100644 --- a/ui/src/views/infra/HostUpdate.vue +++ b/ui/src/views/infra/HostUpdate.vue @@ -64,7 +64,22 @@ - + + + + + + + + + + @@ -149,6 +164,7 @@ export default { methods: { initForm () { this.formRef = ref() + const guestOsRule = this.resource?.guestosrule this.form = reactive({ name: this.resource.name, hosttags: this.resource.explicithosttags, @@ -157,7 +173,9 @@ export default { ? this.resource.storageaccessgroups.split(',') : [], oscategoryid: this.resource.oscategoryid, - externaldetails: this.resourceExternalDetails + externaldetails: this.resourceExternalDetails, + guestosasrule: guestOsRule !== undefined, + guestosrule: guestOsRule }) this.rules = reactive({}) }, @@ -194,7 +212,11 @@ export default { params.id = this.resource.id params.name = values.name params.hosttags = values.hosttags - params.oscategoryid = values.oscategoryid + if (values.guestosasrule === true) { + params.guestosrule = values.guestosrule + } else { + params.oscategoryid = values.oscategoryid || this.osCategories.opts.filter(os => os.name === 'None')[0]?.id + } if (values.istagarule !== undefined) { params.istagarule = values.istagarule } @@ -205,9 +227,11 @@ export default { } else { params.cleanupexternaldetails = true } + + Object.keys(params).forEach((key) => (params[key] == null) && delete params[key]) this.loading = true - postAPI('updateHost', params).then(json => { + postAPI('updateHost', {}, 'POST', params).then(() => { this.$message.success({ content: `${this.$t('label.action.update.host')} - ${values.name}`, duration: 2 diff --git a/utils/src/main/java/org/apache/cloudstack/utils/jsinterpreter/TagAsRuleHelper.java b/utils/src/main/java/org/apache/cloudstack/utils/jsinterpreter/GenericRuleHelper.java similarity index 52% rename from utils/src/main/java/org/apache/cloudstack/utils/jsinterpreter/TagAsRuleHelper.java rename to utils/src/main/java/org/apache/cloudstack/utils/jsinterpreter/GenericRuleHelper.java index b30135673524..7e26563d5d37 100644 --- a/utils/src/main/java/org/apache/cloudstack/utils/jsinterpreter/TagAsRuleHelper.java +++ b/utils/src/main/java/org/apache/cloudstack/utils/jsinterpreter/GenericRuleHelper.java @@ -16,41 +16,57 @@ //under the License. package org.apache.cloudstack.utils.jsinterpreter; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import com.cloud.utils.StringUtils; +import com.cloud.utils.exception.CloudRuntimeException; +import org.apache.commons.lang3.StringEscapeUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import com.cloud.utils.exception.CloudRuntimeException; +import java.io.IOException; -public class TagAsRuleHelper { +public class GenericRuleHelper { - protected static Logger LOGGER = LogManager.getLogger(TagAsRuleHelper.class); + protected static Logger LOGGER = LogManager.getLogger(GenericRuleHelper.class); - public static boolean interpretTagAsRule(String rule, String tags, long timeout) { - List tagsPresetVariable = new ArrayList<>(); - if (!StringUtils.isEmpty(tags)) { - tagsPresetVariable.addAll(Arrays.asList(tags.split(","))); + private static final String PARSE_TAGS = "tags = tags ? tags.split(',') : [];"; + + + public static boolean interpretTagAsRule(String rule, String tags, long timeout, String configName) { + String script = PARSE_TAGS + rule; + Boolean scriptReturn = interpretRule(tags, timeout, "tags", script, configName); + + if (scriptReturn != null) { + return scriptReturn; } - try (JsInterpreter jsInterpreter = new JsInterpreter(timeout)) { - jsInterpreter.injectVariable("tags", tagsPresetVariable); - Object scriptReturn = jsInterpreter.executeScript(rule); + LOGGER.debug("Result of tag rule [{}] was not a boolean, returning false.", script); + return false; + } + + public static boolean interpretGuestOsRule(String rule, String vmGuestOs, long timeout, String configName) { + Boolean scriptReturn = interpretRule(vmGuestOs, timeout, "vmGuestOs", rule, configName); + + if (scriptReturn != null) { + return scriptReturn; + } + + LOGGER.debug("Result of guest OS rule [{}] was not a boolean, returning false.", rule); + return false; + } + + private static Boolean interpretRule(String rule, long timeout, String variable, String script, String configName) { + rule = String.format("'%s'", StringEscapeUtils.escapeEcmaScript(rule)); + try (JsInterpreter jsInterpreter = new JsInterpreter(timeout, configName)) { + jsInterpreter.injectVariable(variable, rule); + Object scriptReturn = jsInterpreter.executeScript(script); if (scriptReturn instanceof Boolean) { - return (Boolean)scriptReturn; + return (Boolean) scriptReturn; } } catch (IOException ex) { - String message = String.format("Error while executing script [%s].", rule); + String message = String.format("Error while executing script [%s].", script); LOGGER.error(message, ex); throw new CloudRuntimeException(message, ex); } - - LOGGER.debug("Result of tag rule [{}] was not a boolean, returning false.", rule); - return false; + return null; } -} +} \ No newline at end of file diff --git a/utils/src/main/java/org/apache/cloudstack/utils/jsinterpreter/JsInterpreter.java b/utils/src/main/java/org/apache/cloudstack/utils/jsinterpreter/JsInterpreter.java index 6e6ef2bbe599..85799e0f5614 100644 --- a/utils/src/main/java/org/apache/cloudstack/utils/jsinterpreter/JsInterpreter.java +++ b/utils/src/main/java/org/apache/cloudstack/utils/jsinterpreter/JsInterpreter.java @@ -86,10 +86,10 @@ static final class DenyAllClassFilter implements ClassFilter { */ protected JsInterpreter() { } - public JsInterpreter(long timeout) { + public JsInterpreter(long timeout, String configName) { this.timeout = timeout; - this.timeoutDefaultMessage = String.format( - "Timeout (in milliseconds) defined in the global setting [quota.activationrule.timeout]: [%s].", this.timeout); + this.timeoutDefaultMessage = String.format("Timeout (in milliseconds) defined in the global setting [%s]: [%s].", + configName, this.timeout); if (System.getProperty("nashorn.args") == null) { System.setProperty("nashorn.args", "--no-java --no-syntax-extensions"); From 95f3376f6ec0993e321e7730a5a08ffb1f55b3a4 Mon Sep 17 00:00:00 2001 From: Fabricio Duarte Date: Thu, 7 May 2026 14:32:04 -0300 Subject: [PATCH 2/3] Fix build --- .../src/main/java/com/cloud/host/HostVO.java | 3 ++- .../allocator/impl/FirstFitAllocatorTest.java | 4 +++ .../allocator/impl/RandomAllocatorTest.java | 8 ++++++ .../jsinterpreter/GenericRuleHelper.java | 25 +++++++++++-------- .../jsinterpreter/JsInterpreterTest.java | 4 +-- 5 files changed, 30 insertions(+), 14 deletions(-) diff --git a/engine/schema/src/main/java/com/cloud/host/HostVO.java b/engine/schema/src/main/java/com/cloud/host/HostVO.java index dc10c539a026..468caf1e2271 100644 --- a/engine/schema/src/main/java/com/cloud/host/HostVO.java +++ b/engine/schema/src/main/java/com/cloud/host/HostVO.java @@ -856,7 +856,8 @@ public boolean checkHostServiceOfferingTags(ServiceOffering serviceOffering) { } if (BooleanUtils.isTrue(this.getIsTagARule())) { - return GenericRuleHelper.interpretTagAsRule(this.getHostTags().get(0), serviceOffering.getHostTag(), HostTagsDao.hostTagRuleExecutionTimeout.value()); + return GenericRuleHelper.interpretTagAsRule(this.getHostTags().get(0), serviceOffering.getHostTag(), HostTagsDao.hostTagRuleExecutionTimeout.value(), + HostTagsDao.hostTagRuleExecutionTimeout.key()); } if (StringUtils.isEmpty(serviceOffering.getHostTag())) { diff --git a/server/src/test/java/com/cloud/agent/manager/allocator/impl/FirstFitAllocatorTest.java b/server/src/test/java/com/cloud/agent/manager/allocator/impl/FirstFitAllocatorTest.java index 076e256eeaf6..f39ed06a656b 100644 --- a/server/src/test/java/com/cloud/agent/manager/allocator/impl/FirstFitAllocatorTest.java +++ b/server/src/test/java/com/cloud/agent/manager/allocator/impl/FirstFitAllocatorTest.java @@ -245,6 +245,7 @@ public void retrieveHostsTestHostsToFilterIsNullAndHaTagNotNullShouldReturnOnlyH Mockito.doReturn(hostsWithHaTag).when(hostDaoMock).listByHostTag(Mockito.any(Host.Type.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyString()); Mockito.doNothing().when(firstFitAllocatorSpy).filterHostsWithUefiEnabled(Mockito.any(Host.Type.class), Mockito.any(VirtualMachineProfile.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyList()); Mockito.doNothing().when(firstFitAllocatorSpy).addHostsBasedOnTagRules(Mockito.anyString(), Mockito.anyList()); + Mockito.doNothing().when(firstFitAllocatorSpy).filterHostsBasedOnGuestOsRules(Mockito.any(VMTemplateVO.class), Mockito.anyList()); List resultHosts = firstFitAllocatorSpy.retrieveHosts(virtualMachineProfile, type, emptyList, vmTemplateVO, clusterId, podId, dcId, hostTag, templateTag); Assert.assertEquals(2, resultHosts.size()); @@ -262,6 +263,7 @@ public void retrieveHostsTestHostsToFilterIsNotNullAndHaTagNotNullShouldReturnOn Mockito.doReturn(hostsWithHaTag).when(hostDaoMock).listByHostTag(Mockito.any(Host.Type.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyString()); Mockito.doNothing().when(firstFitAllocatorSpy).filterHostsWithUefiEnabled(Mockito.any(Host.Type.class), Mockito.any(VirtualMachineProfile.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyList()); Mockito.doNothing().when(firstFitAllocatorSpy).addHostsBasedOnTagRules(Mockito.anyString(), Mockito.anyList()); + Mockito.doNothing().when(firstFitAllocatorSpy).filterHostsBasedOnGuestOsRules(Mockito.any(VMTemplateVO.class), Mockito.anyList()); List resultHosts = firstFitAllocatorSpy.retrieveHosts(virtualMachineProfile, type, hostsToFilter, vmTemplateVO, clusterId, podId, dcId, hostTag, templateTag); Assert.assertEquals(2, resultHosts.size()); @@ -278,6 +280,7 @@ public void retrieveHostsTestHostsToFilterIsNullAndNoHaTagAndNoHostTagShouldRetu Mockito.doReturn(upAndEnabledHostsWithNoHa).when(resourceManagerMock).listAllUpAndEnabledNonHAHosts(Mockito.any(Host.Type.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong()); Mockito.doNothing().when(firstFitAllocatorSpy).filterHostsWithUefiEnabled(Mockito.any(Host.Type.class), Mockito.any(VirtualMachineProfile.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyList()); Mockito.doNothing().when(firstFitAllocatorSpy).addHostsBasedOnTagRules(Mockito.nullable(String.class), Mockito.anyList()); + Mockito.doNothing().when(firstFitAllocatorSpy).filterHostsBasedOnGuestOsRules(Mockito.any(VMTemplateVO.class), Mockito.anyList()); List resultHosts = firstFitAllocatorSpy.retrieveHosts(virtualMachineProfile, type, emptyList, vmTemplateVO, clusterId, podId, dcId, null, null); Assert.assertEquals(2, resultHosts.size()); @@ -293,6 +296,7 @@ public void retrieveHostsTestHostsToFilterIsNullAndNoHaTagWithHostTagShouldCallR Mockito.doNothing().when(firstFitAllocatorSpy).retainHostsMatchingServiceOfferingAndTemplateTags(Mockito.anyList(), Mockito.any(Host.Type.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyString(), Mockito.anyString()); Mockito.doNothing().when(firstFitAllocatorSpy).filterHostsWithUefiEnabled(Mockito.any(Host.Type.class), Mockito.any(VirtualMachineProfile.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyList()); Mockito.doNothing().when(firstFitAllocatorSpy).addHostsBasedOnTagRules(Mockito.anyString(), Mockito.anyList()); + Mockito.doNothing().when(firstFitAllocatorSpy).filterHostsBasedOnGuestOsRules(Mockito.any(VMTemplateVO.class), Mockito.anyList()); firstFitAllocatorSpy.retrieveHosts(virtualMachineProfile, type, emptyList, vmTemplateVO, clusterId, podId, dcId, hostTag, templateTag); Mockito.verify(firstFitAllocatorSpy, Mockito.times(1)).retainHostsMatchingServiceOfferingAndTemplateTags(Mockito.anyList(), Mockito.any(Host.Type.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyString(), Mockito.anyString()); diff --git a/server/src/test/java/com/cloud/agent/manager/allocator/impl/RandomAllocatorTest.java b/server/src/test/java/com/cloud/agent/manager/allocator/impl/RandomAllocatorTest.java index ab9b322eb843..938ed7a152ea 100644 --- a/server/src/test/java/com/cloud/agent/manager/allocator/impl/RandomAllocatorTest.java +++ b/server/src/test/java/com/cloud/agent/manager/allocator/impl/RandomAllocatorTest.java @@ -222,6 +222,7 @@ public void retrieveHostsTestProvidedHostsNullAndNoHostTagAndNoTagRuleShouldOnly Mockito.doReturn(upAndEnabledHosts).when(resourceManagerMock).listAllUpAndEnabledHosts(Mockito.any(Host.Type.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong()); Mockito.doReturn(hostsWithNoRuleTagsAndHostTags).when(hostDao).listAllHostsThatHaveNoRuleTag(Mockito.any(Host.Type.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong()); Mockito.doReturn(emptyList).when(hostDao).findHostsWithTagRuleThatMatchComputeOfferingTags(Mockito.nullable(String.class)); + Mockito.doNothing().when(randomAllocator).filterHostsBasedOnGuestOsRules(Mockito.any(VMTemplateVO.class), Mockito.anyList()); List availableHosts = randomAllocator.retrieveHosts(type, null, vmTemplateVO, null, clusterId, podId, zoneId); Assert.assertEquals(1, availableHosts.size()); @@ -237,6 +238,7 @@ public void retrieveHostsTestProvidedHostsNullAndOnlyHostTagsRulesShouldReturnHo Mockito.doReturn(upAndEnabledHosts).when(resourceManagerMock).listAllUpAndEnabledHosts(Mockito.any(Host.Type.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong()); Mockito.doReturn(hostsWithNoRuleTagsAndHostTags).when(hostDao).listAllHostsThatHaveNoRuleTag(Mockito.any(Host.Type.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong()); Mockito.doReturn(hostsMatchingRuleTags).when(hostDao).findHostsWithTagRuleThatMatchComputeOfferingTags(Mockito.nullable(String.class)); + Mockito.doNothing().when(randomAllocator).filterHostsBasedOnGuestOsRules(Mockito.any(VMTemplateVO.class), Mockito.anyList()); List availableHosts = randomAllocator.retrieveHosts(type, null, vmTemplateVO, null, clusterId, podId, zoneId); Assert.assertEquals(2, availableHosts.size()); @@ -252,6 +254,7 @@ public void retrieveHostsTestProvidedHostsNullProvidedHostTagsNotNullAndNoHostWi Mockito.doReturn(upAndEnabledHosts).when(resourceManagerMock).listAllUpAndEnabledHosts(Mockito.any(Host.Type.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong()); Mockito.doReturn(hostsWithMatchingTags).when(hostDao).listByHostTag(Mockito.any(Host.Type.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyString()); Mockito.doReturn(emptyList).when(hostDao).findHostsWithTagRuleThatMatchComputeOfferingTags(Mockito.nullable(String.class)); + Mockito.doNothing().when(randomAllocator).filterHostsBasedOnGuestOsRules(Mockito.any(VMTemplateVO.class), Mockito.anyList()); List availableHosts = randomAllocator.retrieveHosts(type, null, vmTemplateVO, hostTag, clusterId, podId, zoneId); Assert.assertEquals(1, availableHosts.size()); @@ -267,6 +270,7 @@ public void retrieveHostsTestProvidedHostsNullProvidedHostTagsNotNullAndHostWith Mockito.doReturn(upAndEnabledHosts).when(resourceManagerMock).listAllUpAndEnabledHosts(Mockito.any(Host.Type.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong()); Mockito.doReturn(hostsWithMatchingTags).when(hostDao).listByHostTag(Mockito.any(Host.Type.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyString()); Mockito.doReturn(hostsMatchingRuleTags).when(hostDao).findHostsWithTagRuleThatMatchComputeOfferingTags(Mockito.nullable(String.class)); + Mockito.doNothing().when(randomAllocator).filterHostsBasedOnGuestOsRules(Mockito.any(VMTemplateVO.class), Mockito.anyList()); List availableHosts = randomAllocator.retrieveHosts(type, null, vmTemplateVO, hostTag, clusterId, podId, zoneId); Assert.assertEquals(2, availableHosts.size()); @@ -281,6 +285,7 @@ public void retrieveHostsTestProvidedHostsNotNullAndNoHostTagAndNoTagRuleShouldO Mockito.doReturn(hostsWithNoRuleTagsAndHostTags).when(hostDao).listAllHostsThatHaveNoRuleTag(Mockito.any(Host.Type.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong()); Mockito.doReturn(emptyList).when(hostDao).findHostsWithTagRuleThatMatchComputeOfferingTags(Mockito.nullable(String.class)); + Mockito.doNothing().when(randomAllocator).filterHostsBasedOnGuestOsRules(Mockito.any(VMTemplateVO.class), Mockito.anyList()); List availableHosts = randomAllocator.retrieveHosts(type, providedHosts, vmTemplateVO, null, clusterId, podId, zoneId); Assert.assertEquals(1, availableHosts.size()); @@ -295,6 +300,7 @@ public void retrieveHostsTestProvidedHostsNotNullAndOnlyHostTagsRulesShouldRetur Mockito.doReturn(hostsWithNoRuleTagsAndHostTags).when(hostDao).listAllHostsThatHaveNoRuleTag(Mockito.any(Host.Type.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong()); Mockito.doReturn(hostsMatchingRuleTags).when(hostDao).findHostsWithTagRuleThatMatchComputeOfferingTags(Mockito.nullable(String.class)); + Mockito.doNothing().when(randomAllocator).filterHostsBasedOnGuestOsRules(Mockito.any(VMTemplateVO.class), Mockito.anyList()); List availableHosts = randomAllocator.retrieveHosts(type, providedHosts, vmTemplateVO, null, clusterId, podId, zoneId); Assert.assertEquals(2, availableHosts.size()); @@ -309,6 +315,7 @@ public void retrieveHostsTestProvidedHostsNotNullProvidedHostTagsNotNullAndNoHos Mockito.doReturn(hostsWithMatchingTags).when(hostDao).listByHostTag(Mockito.any(Host.Type.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyString()); Mockito.doReturn(emptyList).when(hostDao).findHostsWithTagRuleThatMatchComputeOfferingTags(Mockito.nullable(String.class)); + Mockito.doNothing().when(randomAllocator).filterHostsBasedOnGuestOsRules(Mockito.any(VMTemplateVO.class), Mockito.anyList()); List availableHosts = randomAllocator.retrieveHosts(type, providedHosts, vmTemplateVO, hostTag, clusterId, podId, zoneId); Assert.assertEquals(1, availableHosts.size()); @@ -323,6 +330,7 @@ public void retrieveHostsTestProvidedHostsNullNotProvidedHostTagsNotNullAndHostW Mockito.doReturn(hostsWithMatchingTags).when(hostDao).listByHostTag(Mockito.any(Host.Type.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyString()); Mockito.doReturn(hostsMatchingRuleTags).when(hostDao).findHostsWithTagRuleThatMatchComputeOfferingTags(Mockito.nullable(String.class)); + Mockito.doNothing().when(randomAllocator).filterHostsBasedOnGuestOsRules(Mockito.any(VMTemplateVO.class), Mockito.anyList()); List availableHosts = randomAllocator.retrieveHosts(type, providedHosts, vmTemplateVO, hostTag, clusterId, podId, zoneId); Assert.assertEquals(2, availableHosts.size()); diff --git a/utils/src/main/java/org/apache/cloudstack/utils/jsinterpreter/GenericRuleHelper.java b/utils/src/main/java/org/apache/cloudstack/utils/jsinterpreter/GenericRuleHelper.java index 7e26563d5d37..926ce05168a1 100644 --- a/utils/src/main/java/org/apache/cloudstack/utils/jsinterpreter/GenericRuleHelper.java +++ b/utils/src/main/java/org/apache/cloudstack/utils/jsinterpreter/GenericRuleHelper.java @@ -16,34 +16,38 @@ //under the License. package org.apache.cloudstack.utils.jsinterpreter; +import com.cloud.utils.StringUtils; import com.cloud.utils.exception.CloudRuntimeException; -import org.apache.commons.lang3.StringEscapeUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; public class GenericRuleHelper { protected static Logger LOGGER = LogManager.getLogger(GenericRuleHelper.class); - private static final String PARSE_TAGS = "tags = tags ? tags.split(',') : [];"; - - public static boolean interpretTagAsRule(String rule, String tags, long timeout, String configName) { - String script = PARSE_TAGS + rule; - Boolean scriptReturn = interpretRule(tags, timeout, "tags", script, configName); + List tagsPresetVariable = new ArrayList<>(); + if (!StringUtils.isEmpty(tags)) { + tagsPresetVariable.addAll(Arrays.asList(tags.split(","))); + } + + Boolean scriptReturn = interpretRule("tags", tagsPresetVariable, timeout, rule, configName); if (scriptReturn != null) { return scriptReturn; } - LOGGER.debug("Result of tag rule [{}] was not a boolean, returning false.", script); + LOGGER.debug("Result of tag rule [{}] was not a boolean, returning false.", rule); return false; } public static boolean interpretGuestOsRule(String rule, String vmGuestOs, long timeout, String configName) { - Boolean scriptReturn = interpretRule(vmGuestOs, timeout, "vmGuestOs", rule, configName); + Boolean scriptReturn = interpretRule("vmGuestOs", vmGuestOs, timeout, rule, configName); if (scriptReturn != null) { return scriptReturn; @@ -53,10 +57,9 @@ public static boolean interpretGuestOsRule(String rule, String vmGuestOs, long t return false; } - private static Boolean interpretRule(String rule, long timeout, String variable, String script, String configName) { - rule = String.format("'%s'", StringEscapeUtils.escapeEcmaScript(rule)); + private static Boolean interpretRule(String variableName, Object variableValue, long timeout, String script, String configName) { try (JsInterpreter jsInterpreter = new JsInterpreter(timeout, configName)) { - jsInterpreter.injectVariable(variable, rule); + jsInterpreter.injectVariable(variableName, variableValue); Object scriptReturn = jsInterpreter.executeScript(script); if (scriptReturn instanceof Boolean) { return (Boolean) scriptReturn; diff --git a/utils/src/test/java/org/apache/cloudstack/utils/jsinterpreter/JsInterpreterTest.java b/utils/src/test/java/org/apache/cloudstack/utils/jsinterpreter/JsInterpreterTest.java index e6ef43f2fc10..e4764b2af413 100644 --- a/utils/src/test/java/org/apache/cloudstack/utils/jsinterpreter/JsInterpreterTest.java +++ b/utils/src/test/java/org/apache/cloudstack/utils/jsinterpreter/JsInterpreterTest.java @@ -161,7 +161,7 @@ public void setScriptEngineDisablingJavaLanguageTest() { @Test public void executeScriptTestValidScriptShouldPassWithMixedVariables() { - try (JsInterpreter jsInterpreter = new JsInterpreter(1000)) { + try (JsInterpreter jsInterpreter = new JsInterpreter(1000, "timeout.configuration")) { jsInterpreter.injectVariable("x", 10); jsInterpreter.injectVariable("y", "hello"); jsInterpreter.injectVariable("z", true); @@ -174,7 +174,7 @@ public void executeScriptTestValidScriptShouldPassWithMixedVariables() { } private void runMaliciousScriptFileTest(String script, String filename) { - try (JsInterpreter jsInterpreter = new JsInterpreter(1000)) { + try (JsInterpreter jsInterpreter = new JsInterpreter(1000, "timeout.configuration")) { jsInterpreter.executeScript(script); } catch (CloudRuntimeException ex) { Assert.assertTrue(ex.getMessage().contains("Unable to execute script")); From 8a53071742ca8d647cb5ef4392861089b19a7403 Mon Sep 17 00:00:00 2001 From: Fabricio Duarte Date: Thu, 7 May 2026 15:14:39 -0300 Subject: [PATCH 3/3] Address pre-commit failures --- .../apache/cloudstack/api/command/admin/host/UpdateHostCmd.java | 2 +- .../cloudstack/utils/jsinterpreter/GenericRuleHelper.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/host/UpdateHostCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/host/UpdateHostCmd.java index 8e66b2256707..69bca7c79e21 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/host/UpdateHostCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/host/UpdateHostCmd.java @@ -49,7 +49,7 @@ public class UpdateHostCmd extends BaseCmd { @Parameter(name = ApiConstants.OS_CATEGORY_ID, type = CommandType.UUID, entityType = GuestOSCategoryResponse.class, - description = "the ID of OS category used to prioritize VMs with mathing OS category during the allocation process. " + + description = "the ID of OS category used to prioritize VMs with matching OS category during the allocation process. " + "It cannot be used alongside the 'guestosrule' parameter.") private Long osCategoryId; diff --git a/utils/src/main/java/org/apache/cloudstack/utils/jsinterpreter/GenericRuleHelper.java b/utils/src/main/java/org/apache/cloudstack/utils/jsinterpreter/GenericRuleHelper.java index 926ce05168a1..506bbc1ea5c3 100644 --- a/utils/src/main/java/org/apache/cloudstack/utils/jsinterpreter/GenericRuleHelper.java +++ b/utils/src/main/java/org/apache/cloudstack/utils/jsinterpreter/GenericRuleHelper.java @@ -72,4 +72,4 @@ private static Boolean interpretRule(String variableName, Object variableValue, return null; } -} \ No newline at end of file +}