From af17bc916b4996276ea497234d0be5b6482833e9 Mon Sep 17 00:00:00 2001 From: Fabricio Duarte Date: Mon, 11 May 2026 17:14:42 -0300 Subject: [PATCH 1/3] Introduce Quota resource statement API --- .../apache/cloudstack/api/ApiConstants.java | 4 + .../META-INF/db/schema-42210to42300.sql | 6 + .../cloudstack/quota/QuotaManagerImpl.java | 90 +++++-- .../cloudstack/quota/constant/QuotaTypes.java | 90 ++++--- .../quota/dao/QuotaUsageJoinDao.java | 2 +- .../quota/dao/QuotaUsageJoinDaoImpl.java | 8 +- .../quota/QuotaManagerImplTest.java | 74 +++++- .../command/QuotaResourceStatementCmd.java | 118 +++++++++ .../api/command/QuotaStatementCmd.java | 15 +- .../QuotaResourceStatementItemResponse.java | 82 ++++++ .../QuotaResourceStatementResponse.java | 66 +++++ .../api/response/QuotaResponseBuilder.java | 3 + .../response/QuotaResponseBuilderImpl.java | 242 ++++++++++++++---- .../api/response/QuotaStatementResponse.java | 4 +- .../apache/cloudstack/quota/QuotaService.java | 2 +- .../cloudstack/quota/QuotaServiceImpl.java | 10 +- .../QuotaResponseBuilderImplTest.java | 150 ++++++----- .../quota/QuotaServiceImplTest.java | 6 +- 18 files changed, 771 insertions(+), 201 deletions(-) create mode 100644 plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaResourceStatementCmd.java create mode 100644 plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResourceStatementItemResponse.java create mode 100644 plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResourceStatementResponse.java 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..c1b195d8de6a 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -595,6 +595,8 @@ public class ApiConstants { public static final String SUITABLE_FOR_VM = "suitableforvirtualmachine"; public static final String SUPPORTS_STORAGE_SNAPSHOT = "supportsstoragesnapshot"; public static final String TARGET_IQN = "targetiqn"; + public static final String TARIFF_ID = "tariffid"; + public static final String TARIFF_NAME = "tariffname"; public static final String TASKS_FILTER = "tasksfilter"; public static final String TEMPLATE_FILTER = "templatefilter"; public static final String TEMPLATE_ID = "templateid"; @@ -655,6 +657,7 @@ public class ApiConstants { public static final String VIRTUAL_MACHINE_STATE = "vmstate"; public static final String VIRTUAL_MACHINES = "virtualmachines"; public static final String USAGE_ID = "usageid"; + public static final String USAGE_NAME = "usagename"; public static final String USAGE_TYPE = "usagetype"; public static final String INCLUDE_TAGS = "includetags"; @@ -871,6 +874,7 @@ public class ApiConstants { public static final String IS_SOURCE_NAT = "issourcenat"; public static final String IS_STATIC_NAT = "isstaticnat"; public static final String ITERATIONS = "iterations"; + public static final String ITEMS = "items"; public static final String SORT_BY = "sortby"; public static final String CHANGE_CIDR = "changecidr"; public static final String PURPOSE = "purpose"; diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql b/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql index c99f798d3d56..8cd368cde165 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql @@ -131,3 +131,9 @@ CREATE TABLE IF NOT EXISTS `cloud_usage`.`quota_tariff_usage` ( -- Add the 'keep_mac_address_on_public_nic' column to the 'cloud.networks' and 'cloud.vpc' tables CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.networks', 'keep_mac_address_on_public_nic', 'TINYINT(1) NOT NULL DEFAULT 1'); CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.vpc', 'keep_mac_address_on_public_nic', 'TINYINT(1) NOT NULL DEFAULT 1'); + +--- Quota resource statement +INSERT INTO cloud.role_permissions (uuid, role_id, rule, permission, sort_order) +SELECT uuid(), role_id, 'quotaResourceStatement', permission, sort_order +FROM cloud.role_permissions rp +WHERE rule = 'quotaStatement' AND NOT EXISTS(SELECT 1 FROM cloud.role_permissions rp_ WHERE rp.role_id = rp_.role_id AND rp_.rule = 'quotaResourceStatement'); 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..03434362e66b 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 @@ -23,6 +23,7 @@ import java.util.Comparator; import java.util.Date; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -43,9 +44,11 @@ import org.apache.cloudstack.quota.dao.QuotaAccountDao; import org.apache.cloudstack.quota.dao.QuotaBalanceDao; import org.apache.cloudstack.quota.dao.QuotaTariffDao; +import org.apache.cloudstack.quota.dao.QuotaTariffUsageDao; import org.apache.cloudstack.quota.dao.QuotaUsageDao; import org.apache.cloudstack.quota.vo.QuotaAccountVO; import org.apache.cloudstack.quota.vo.QuotaBalanceVO; +import org.apache.cloudstack.quota.vo.QuotaTariffUsageVO; import org.apache.cloudstack.quota.vo.QuotaTariffVO; import org.apache.cloudstack.quota.vo.QuotaUsageVO; import org.apache.cloudstack.usage.UsageUnitTypes; @@ -86,7 +89,8 @@ public class QuotaManagerImpl extends ManagerBase implements QuotaManager { private QuotaBalanceDao _quotaBalanceDao; @Inject private ConfigurationDao _configDao; - + @Inject + private QuotaTariffUsageDao quotaTariffUsageDao; @Inject protected PresetVariableHelper presetVariableHelper; @@ -310,14 +314,14 @@ protected List createQuotaUsagesAccordingToQuotaTariffs(AccountVO String accountToString = account.reflectionToString(); logger.info("Calculating quota usage of [{}] usage records for account [{}].", usageRecords.size(), accountToString); - List> pairsUsageAndQuotaUsage = new ArrayList<>(); + Map>> mapUsageAndQuotaUsage = new LinkedHashMap<>(); try (JsInterpreter jsInterpreter = new JsInterpreter(QuotaConfig.QuotaActivationRuleTimeout.value())) { for (UsageVO usageRecord : usageRecords) { int usageType = usageRecord.getUsageType(); if (!shouldCalculateUsageRecord(account, usageRecord)) { - pairsUsageAndQuotaUsage.add(new Pair<>(usageRecord, null)); + mapUsageAndQuotaUsage.put(usageRecord, null); continue; } @@ -325,18 +329,31 @@ protected List createQuotaUsagesAccordingToQuotaTariffs(AccountVO List quotaTariffs = pairQuotaTariffsPerUsageTypeAndHasActivationRule.first(); boolean hasAnyQuotaTariffWithActivationRule = pairQuotaTariffsPerUsageTypeAndHasActivationRule.second(); - BigDecimal aggregatedQuotaTariffsValue = aggregateQuotaTariffsValues(usageRecord, quotaTariffs, hasAnyQuotaTariffWithActivationRule, jsInterpreter, accountToString); + Map aggregatedQuotaTariffsAndValues = aggregateQuotaTariffsValues(usageRecord, + quotaTariffs, hasAnyQuotaTariffWithActivationRule, jsInterpreter, accountToString); + BigDecimal aggregatedQuotaTariffsValue = aggregatedQuotaTariffsAndValues.values().stream().reduce(BigDecimal.ZERO, BigDecimal::add); + logger.debug("The aggregation of the quota tariffs of account [{}] resulted in [{}] for the usage record [{}].", + account, aggregatedQuotaTariffsValue, usageRecord); QuotaUsageVO quotaUsage = createQuotaUsageAccordingToUsageUnit(usageRecord, aggregatedQuotaTariffsValue, accountToString); + if (quotaUsage == null) { + mapUsageAndQuotaUsage.put(usageRecord, null); + continue; + } - pairsUsageAndQuotaUsage.add(new Pair<>(usageRecord, quotaUsage)); + List quotaTariffUsages = new ArrayList<>(); + for (Map.Entry entry : aggregatedQuotaTariffsAndValues.entrySet()) { + QuotaTariffUsageVO quotaTariffUsage = createQuotaTariffUsage(usageRecord, entry.getKey(), entry.getValue()); + quotaTariffUsages.add(quotaTariffUsage); + } + mapUsageAndQuotaUsage.put(usageRecord, new Pair<>(quotaUsage, quotaTariffUsages)); } } catch (Exception e) { logger.error(String.format("Failed to calculate the quota usage for account [%s] due to [%s].", accountToString, e.getMessage()), e); return new ArrayList<>(); } - return persistUsagesAndQuotaUsagesAndRetrievePersistedQuotaUsages(pairsUsageAndQuotaUsage); + return persistUsagesAndQuotaUsagesAndRetrievePersistedQuotaUsages(mapUsageAndQuotaUsage); } protected boolean shouldCalculateUsageRecord(AccountVO accountVO, UsageVO usageRecord) { @@ -348,31 +365,41 @@ protected boolean shouldCalculateUsageRecord(AccountVO accountVO, UsageVO usageR return true; } - protected List persistUsagesAndQuotaUsagesAndRetrievePersistedQuotaUsages(List> pairsUsageAndQuotaUsage) { - List quotaUsages = new ArrayList<>(); + protected List persistUsagesAndQuotaUsagesAndRetrievePersistedQuotaUsages(Map>> mapUsageAndQuotaTariffUsage) { + List quotaUsages = new ArrayList<>(); // TODO: isso tem que ser em uma transação - for (Pair pairUsageAndQuotaUsage : pairsUsageAndQuotaUsage) { - UsageVO usageVo = pairUsageAndQuotaUsage.first(); + for (Map.Entry>> usageAndTariffUsage : mapUsageAndQuotaTariffUsage.entrySet()) { + UsageVO usageVo = usageAndTariffUsage.getKey(); usageVo.setQuotaCalculated(1); _usageDao.persistUsage(usageVo); - QuotaUsageVO quotaUsageVo = pairUsageAndQuotaUsage.second(); - if (quotaUsageVo != null) { - _quotaUsageDao.persistQuotaUsage(quotaUsageVo); - quotaUsages.add(quotaUsageVo); + Pair> pairUsageAndTariffUsages = usageAndTariffUsage.getValue(); + if (pairUsageAndTariffUsages != null) { + QuotaUsageVO quotaUsage = pairUsageAndTariffUsages.first(); + _quotaUsageDao.persistQuotaUsage(quotaUsage); + quotaUsages.add(quotaUsage); + + persistQuotaTariffUsages(pairUsageAndTariffUsages.second(), quotaUsage.getId()); } } return quotaUsages; } - protected BigDecimal aggregateQuotaTariffsValues(UsageVO usageRecord, List quotaTariffs, boolean hasAnyQuotaTariffWithActivationRule, - JsInterpreter jsInterpreter, String accountToString) { + protected void persistQuotaTariffUsages(List quotaTariffUsages, Long quotaUsageId) { + for (QuotaTariffUsageVO quotaTariffUsage : quotaTariffUsages) { + quotaTariffUsage.setQuotaUsageId(quotaUsageId); + quotaTariffUsageDao.persistQuotaTariffUsage(quotaTariffUsage); + } + } + + protected Map aggregateQuotaTariffsValues(UsageVO usageRecord, List quotaTariffs, boolean hasAnyQuotaTariffWithActivationRule, + JsInterpreter jsInterpreter, String accountToString) { String usageRecordToString = usageRecord.toString(usageAggregationTimeZone); logger.debug("Validating usage record [{}] for account [{}] against [{}] quota tariffs.", usageRecordToString, accountToString, quotaTariffs.size()); PresetVariables presetVariables = getPresetVariables(hasAnyQuotaTariffWithActivationRule, usageRecord); - BigDecimal aggregatedQuotaTariffsValue = BigDecimal.ZERO; + Map aggregatedQuotaTariffsAndValues = new HashMap<>(); quotaTariffs.sort(Comparator.comparing(QuotaTariffVO::getPosition)); @@ -381,10 +408,9 @@ protected BigDecimal aggregateQuotaTariffsValues(UsageVO usageRecord, List *
    diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/constant/QuotaTypes.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/constant/QuotaTypes.java index 0da0d6e53f77..7b725d57c6ef 100644 --- a/framework/quota/src/main/java/org/apache/cloudstack/quota/constant/QuotaTypes.java +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/constant/QuotaTypes.java @@ -20,11 +20,23 @@ import java.util.HashMap; import java.util.Map; -import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.network.IpAddress; +import com.cloud.network.Network; +import com.cloud.network.VpnUser; +import com.cloud.network.rules.LoadBalancer; +import com.cloud.network.rules.PortForwardingRule; +import com.cloud.network.security.SecurityGroup; +import com.cloud.network.vpc.Vpc; +import com.cloud.offering.NetworkOffering; +import com.cloud.storage.Snapshot; +import com.cloud.storage.Volume; +import com.cloud.template.VirtualMachineTemplate; +import com.cloud.vm.VirtualMachine; +import org.apache.cloudstack.backup.BackupOffering; +import org.apache.cloudstack.storage.object.Bucket; import org.apache.cloudstack.usage.UsageTypes; import org.apache.cloudstack.usage.UsageUnitTypes; import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; -import org.apache.commons.lang3.StringUtils; public class QuotaTypes extends UsageTypes { private final Integer quotaType; @@ -32,44 +44,46 @@ public class QuotaTypes extends UsageTypes { private final String quotaUnit; private final String description; private final String discriminator; + private final Class clazz; private final static Map quotaTypeMap; static { final HashMap quotaTypeList = new HashMap(); - quotaTypeList.put(RUNNING_VM, new QuotaTypes(RUNNING_VM, "RUNNING_VM", UsageUnitTypes.COMPUTE_MONTH.toString(), "Running Vm Usage")); - quotaTypeList.put(ALLOCATED_VM, new QuotaTypes(ALLOCATED_VM, "ALLOCATED_VM", UsageUnitTypes.COMPUTE_MONTH.toString(), "Allocated Vm Usage")); - quotaTypeList.put(IP_ADDRESS, new QuotaTypes(IP_ADDRESS, "IP_ADDRESS", UsageUnitTypes.IP_MONTH.toString(), "IP Address Usage")); - quotaTypeList.put(NETWORK_BYTES_SENT, new QuotaTypes(NETWORK_BYTES_SENT, "NETWORK_BYTES_SENT", UsageUnitTypes.GB.toString(), "Network Usage (Bytes Sent)")); - quotaTypeList.put(NETWORK_BYTES_RECEIVED, new QuotaTypes(NETWORK_BYTES_RECEIVED, "NETWORK_BYTES_RECEIVED", UsageUnitTypes.GB.toString(), "Network Usage (Bytes Received)")); - quotaTypeList.put(VOLUME, new QuotaTypes(VOLUME, "VOLUME", UsageUnitTypes.GB_MONTH.toString(), "Volume Usage")); - quotaTypeList.put(TEMPLATE, new QuotaTypes(TEMPLATE, "TEMPLATE", UsageUnitTypes.GB_MONTH.toString(), "Template Usage")); - quotaTypeList.put(ISO, new QuotaTypes(ISO, "ISO", UsageUnitTypes.GB_MONTH.toString(), "ISO Usage")); - quotaTypeList.put(SNAPSHOT, new QuotaTypes(SNAPSHOT, "SNAPSHOT", UsageUnitTypes.GB_MONTH.toString(), "Snapshot Usage")); - quotaTypeList.put(SECURITY_GROUP, new QuotaTypes(SECURITY_GROUP, "SECURITY_GROUP", UsageUnitTypes.POLICY_MONTH.toString(), "Security Group Usage")); - quotaTypeList.put(LOAD_BALANCER_POLICY, new QuotaTypes(LOAD_BALANCER_POLICY, "LOAD_BALANCER_POLICY", UsageUnitTypes.POLICY_MONTH.toString(), "Load Balancer Usage")); - quotaTypeList.put(PORT_FORWARDING_RULE, new QuotaTypes(PORT_FORWARDING_RULE, "PORT_FORWARDING_RULE", UsageUnitTypes.POLICY_MONTH.toString(), "Port Forwarding Usage")); - quotaTypeList.put(NETWORK_OFFERING, new QuotaTypes(NETWORK_OFFERING, "NETWORK_OFFERING", UsageUnitTypes.POLICY_MONTH.toString(), "Network Offering Usage")); - quotaTypeList.put(VPN_USERS, new QuotaTypes(VPN_USERS, "VPN_USERS", UsageUnitTypes.POLICY_MONTH.toString(), "VPN users usage")); - quotaTypeList.put(VM_DISK_IO_READ, new QuotaTypes(VM_DISK_IO_READ, "VM_DISK_IO_READ", UsageUnitTypes.IOPS.toString(), "VM Disk usage(I/O Read)")); - quotaTypeList.put(VM_DISK_IO_WRITE, new QuotaTypes(VM_DISK_IO_WRITE, "VM_DISK_IO_WRITE", UsageUnitTypes.IOPS.toString(), "VM Disk usage(I/O Write)")); - quotaTypeList.put(VM_DISK_BYTES_READ, new QuotaTypes(VM_DISK_BYTES_READ, "VM_DISK_BYTES_READ", UsageUnitTypes.BYTES.toString(), "VM Disk usage(Bytes Read)")); - quotaTypeList.put(VM_DISK_BYTES_WRITE, new QuotaTypes(VM_DISK_BYTES_WRITE, "VM_DISK_BYTES_WRITE", UsageUnitTypes.BYTES.toString(), "VM Disk usage(Bytes Write)")); - quotaTypeList.put(VM_SNAPSHOT, new QuotaTypes(VM_SNAPSHOT, "VM_SNAPSHOT", UsageUnitTypes.GB_MONTH.toString(), "VM Snapshot storage usage")); - quotaTypeList.put(VOLUME_SECONDARY, new QuotaTypes(VOLUME_SECONDARY, "VOLUME_SECONDARY", UsageUnitTypes.GB_MONTH.toString(), "Volume secondary storage usage")); - quotaTypeList.put(VM_SNAPSHOT_ON_PRIMARY, new QuotaTypes(VM_SNAPSHOT_ON_PRIMARY, "VM_SNAPSHOT_ON_PRIMARY", UsageUnitTypes.GB_MONTH.toString(), "VM Snapshot primary storage usage")); - quotaTypeList.put(BACKUP, new QuotaTypes(BACKUP, "BACKUP", UsageUnitTypes.GB_MONTH.toString(), "Backup storage usage")); - quotaTypeList.put(BUCKET, new QuotaTypes(BUCKET, "BUCKET", UsageUnitTypes.GB_MONTH.toString(), "Object Store bucket usage")); - quotaTypeList.put(NETWORK, new QuotaTypes(NETWORK, "NETWORK", UsageUnitTypes.COMPUTE_MONTH.toString(), "Network usage")); - quotaTypeList.put(VPC, new QuotaTypes(VPC, "VPC", UsageUnitTypes.COMPUTE_MONTH.toString(), "VPC usage")); + quotaTypeList.put(RUNNING_VM, new QuotaTypes(RUNNING_VM, "RUNNING_VM", UsageUnitTypes.COMPUTE_MONTH.toString(), "Running Vm Usage", VirtualMachine.class)); + quotaTypeList.put(ALLOCATED_VM, new QuotaTypes(ALLOCATED_VM, "ALLOCATED_VM", UsageUnitTypes.COMPUTE_MONTH.toString(), "Allocated Vm Usage", VirtualMachine.class)); + quotaTypeList.put(IP_ADDRESS, new QuotaTypes(IP_ADDRESS, "IP_ADDRESS", UsageUnitTypes.IP_MONTH.toString(), "IP Address Usage", IpAddress.class)); + quotaTypeList.put(NETWORK_BYTES_SENT, new QuotaTypes(NETWORK_BYTES_SENT, "NETWORK_BYTES_SENT", UsageUnitTypes.GB.toString(), "Network Usage (Bytes Sent)", Network.class)); + quotaTypeList.put(NETWORK_BYTES_RECEIVED, new QuotaTypes(NETWORK_BYTES_RECEIVED, "NETWORK_BYTES_RECEIVED", UsageUnitTypes.GB.toString(), "Network Usage (Bytes Received)", Network.class)); + quotaTypeList.put(VOLUME, new QuotaTypes(VOLUME, "VOLUME", UsageUnitTypes.GB_MONTH.toString(), "Volume Usage", Volume.class)); + quotaTypeList.put(TEMPLATE, new QuotaTypes(TEMPLATE, "TEMPLATE", UsageUnitTypes.GB_MONTH.toString(), "Template Usage", VirtualMachineTemplate.class)); + quotaTypeList.put(ISO, new QuotaTypes(ISO, "ISO", UsageUnitTypes.GB_MONTH.toString(), "ISO Usage", VirtualMachineTemplate.class)); + quotaTypeList.put(SNAPSHOT, new QuotaTypes(SNAPSHOT, "SNAPSHOT", UsageUnitTypes.GB_MONTH.toString(), "Snapshot Usage", Snapshot.class)); + quotaTypeList.put(SECURITY_GROUP, new QuotaTypes(SECURITY_GROUP, "SECURITY_GROUP", UsageUnitTypes.POLICY_MONTH.toString(), "Security Group Usage", SecurityGroup.class)); + quotaTypeList.put(LOAD_BALANCER_POLICY, new QuotaTypes(LOAD_BALANCER_POLICY, "LOAD_BALANCER_POLICY", UsageUnitTypes.POLICY_MONTH.toString(), "Load Balancer Usage", LoadBalancer.class)); + quotaTypeList.put(PORT_FORWARDING_RULE, new QuotaTypes(PORT_FORWARDING_RULE, "PORT_FORWARDING_RULE", UsageUnitTypes.POLICY_MONTH.toString(), "Port Forwarding Usage", PortForwardingRule.class)); + quotaTypeList.put(NETWORK_OFFERING, new QuotaTypes(NETWORK_OFFERING, "NETWORK_OFFERING", UsageUnitTypes.POLICY_MONTH.toString(), "Network Offering Usage", NetworkOffering.class)); + quotaTypeList.put(VPN_USERS, new QuotaTypes(VPN_USERS, "VPN_USERS", UsageUnitTypes.POLICY_MONTH.toString(), "VPN users usage", VpnUser.class)); + quotaTypeList.put(VM_DISK_IO_READ, new QuotaTypes(VM_DISK_IO_READ, "VM_DISK_IO_READ", UsageUnitTypes.IOPS.toString(), "VM Disk usage(I/O Read)", Volume.class)); + quotaTypeList.put(VM_DISK_IO_WRITE, new QuotaTypes(VM_DISK_IO_WRITE, "VM_DISK_IO_WRITE", UsageUnitTypes.IOPS.toString(), "VM Disk usage(I/O Write)", Volume.class)); + quotaTypeList.put(VM_DISK_BYTES_READ, new QuotaTypes(VM_DISK_BYTES_READ, "VM_DISK_BYTES_READ", UsageUnitTypes.BYTES.toString(), "VM Disk usage(Bytes Read)", Volume.class)); + quotaTypeList.put(VM_DISK_BYTES_WRITE, new QuotaTypes(VM_DISK_BYTES_WRITE, "VM_DISK_BYTES_WRITE", UsageUnitTypes.BYTES.toString(), "VM Disk usage(Bytes Write)", Volume.class)); + quotaTypeList.put(VM_SNAPSHOT, new QuotaTypes(VM_SNAPSHOT, "VM_SNAPSHOT", UsageUnitTypes.GB_MONTH.toString(), "VM Snapshot storage usage", Snapshot.class)); + quotaTypeList.put(VOLUME_SECONDARY, new QuotaTypes(VOLUME_SECONDARY, "VOLUME_SECONDARY", UsageUnitTypes.GB_MONTH.toString(), "Volume secondary storage usage", Volume.class)); + quotaTypeList.put(VM_SNAPSHOT_ON_PRIMARY, new QuotaTypes(VM_SNAPSHOT_ON_PRIMARY, "VM_SNAPSHOT_ON_PRIMARY", UsageUnitTypes.GB_MONTH.toString(), "VM Snapshot primary storage usage", Snapshot.class)); + quotaTypeList.put(BACKUP, new QuotaTypes(BACKUP, "BACKUP", UsageUnitTypes.GB_MONTH.toString(), "Backup storage usage", BackupOffering.class)); + quotaTypeList.put(BUCKET, new QuotaTypes(BUCKET, "BUCKET", UsageUnitTypes.GB_MONTH.toString(), "Object Store bucket usage", Bucket.class)); + quotaTypeList.put(NETWORK, new QuotaTypes(NETWORK, "NETWORK", UsageUnitTypes.COMPUTE_MONTH.toString(), "Network usage", Network.class)); + quotaTypeList.put(VPC, new QuotaTypes(VPC, "VPC", UsageUnitTypes.COMPUTE_MONTH.toString(), "VPC usage", Vpc.class)); quotaTypeMap = Collections.unmodifiableMap(quotaTypeList); } - private QuotaTypes(Integer quotaType, String name, String unit, String description) { + private QuotaTypes(Integer quotaType, String name, String unit, String description, Class clazz) { this.quotaType = quotaType; this.description = description; this.quotaName = name; this.quotaUnit = unit; this.discriminator = "None"; + this.clazz = clazz; } public static Map listQuotaTypes() { @@ -104,22 +118,20 @@ static public String getDescription(int quotaType) { return null; } + public Class getClazz() { + return clazz; + } + static public QuotaTypes getQuotaType(int quotaType) { return quotaTypeMap.get(quotaType); } - static public QuotaTypes getQuotaTypeByName(String name) { - if (StringUtils.isBlank(name)) { - throw new CloudRuntimeException("Could not retrieve Quota type by name because the value passed as parameter is null, empty, or blank."); - } - - for (QuotaTypes type : quotaTypeMap.values()) { - if (type.getQuotaName().equals(name)) { - return type; - } + static public Class getClazz(int quotaType) { + QuotaTypes t = quotaTypeMap.get(quotaType); + if (t != null) { + return t.getClazz(); } - - throw new CloudRuntimeException(String.format("Could not find Quota type with name [%s].", name)); + return null; } @Override diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaUsageJoinDao.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaUsageJoinDao.java index 126fa11413f7..ead70ae35012 100644 --- a/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaUsageJoinDao.java +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaUsageJoinDao.java @@ -26,6 +26,6 @@ public interface QuotaUsageJoinDao extends GenericDao { - List findQuotaUsage(Long accountId, Long domainId, Integer usageType, Long resourceId, Long networkId, Long offeringId, Date startDate, Date endDate, Long tariffId); + List findQuotaUsage(Long accountId, List domainIds, Integer usageType, Long resourceId, Long networkId, Long offeringId, Date startDate, Date endDate, Long tariffId); } diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaUsageJoinDaoImpl.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaUsageJoinDaoImpl.java index b98ea2b3a5d2..e69b1a05ba4d 100644 --- a/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaUsageJoinDaoImpl.java +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaUsageJoinDaoImpl.java @@ -61,7 +61,7 @@ public void init() { private void prepareQuotaUsageSearchBuilder(SearchBuilder searchBuilder) { searchBuilder.and("accountId", searchBuilder.entity().getAccountId(), SearchCriteria.Op.EQ); - searchBuilder.and("domainId", searchBuilder.entity().getDomainId(), SearchCriteria.Op.EQ); + searchBuilder.and("domainIds", searchBuilder.entity().getDomainId(), SearchCriteria.Op.IN); searchBuilder.and("usageType", searchBuilder.entity().getUsageType(), SearchCriteria.Op.EQ); searchBuilder.and("resourceId", searchBuilder.entity().getResourceId(), SearchCriteria.Op.EQ); searchBuilder.and("networkId", searchBuilder.entity().getNetworkId(), SearchCriteria.Op.EQ); @@ -71,11 +71,13 @@ private void prepareQuotaUsageSearchBuilder(SearchBuilder sear } @Override - public List findQuotaUsage(Long accountId, Long domainId, Integer usageType, Long resourceId, Long networkId, Long offeringId, Date startDate, Date endDate, Long tariffId) { + public List findQuotaUsage(Long accountId, List domainIds, Integer usageType, Long resourceId, Long networkId, Long offeringId, Date startDate, Date endDate, Long tariffId) { SearchCriteria sc = tariffId == null ? searchQuotaUsages.create() : searchQuotaUsagesJoinTariffUsages.create(); sc.setParametersIfNotNull("accountId", accountId); - sc.setParametersIfNotNull("domainId", domainId); + if (domainIds != null) { + sc.setParameters("domainIds", domainIds.toArray()); + } sc.setParametersIfNotNull("usageType", usageType); sc.setParametersIfNotNull("resourceId", resourceId); sc.setParametersIfNotNull("networkId", networkId); diff --git a/framework/quota/src/test/java/org/apache/cloudstack/quota/QuotaManagerImplTest.java b/framework/quota/src/test/java/org/apache/cloudstack/quota/QuotaManagerImplTest.java index a33faa054de4..6cecddbd4dbd 100644 --- a/framework/quota/src/test/java/org/apache/cloudstack/quota/QuotaManagerImplTest.java +++ b/framework/quota/src/test/java/org/apache/cloudstack/quota/QuotaManagerImplTest.java @@ -22,6 +22,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Date; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -33,7 +34,9 @@ import org.apache.cloudstack.quota.activationrule.presetvariables.Value; import org.apache.cloudstack.quota.constant.QuotaTypes; import org.apache.cloudstack.quota.dao.QuotaTariffDao; +import org.apache.cloudstack.quota.dao.QuotaTariffUsageDao; import org.apache.cloudstack.quota.dao.QuotaUsageDao; +import org.apache.cloudstack.quota.vo.QuotaTariffUsageVO; import org.apache.cloudstack.quota.vo.QuotaTariffVO; import org.apache.cloudstack.quota.vo.QuotaUsageVO; import org.apache.cloudstack.usage.UsageUnitTypes; @@ -89,6 +92,9 @@ public class QuotaManagerImplTest { @Mock PresetVariables presetVariablesMock; + @Mock + QuotaTariffUsageDao quotaTariffUsageDaoMock; + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); @Test @@ -484,47 +490,49 @@ public void getPresetVariablesTestHasTariffsWithActivationRuleReturnPresetVariab } @Test - public void aggregateQuotaTariffsValuesTestTariffsWereNotInPeriodToBeAppliedReturnZero() { + public void aggregateQuotaTariffsValuesTestTariffsWereNotInPeriodToBeAppliedReturnEmptyMap() { List tariffs = createTariffList(); Mockito.doReturn(false).when(quotaManagerImplSpy).isQuotaTariffInPeriodToBeApplied(Mockito.any(), Mockito.any(), Mockito.anyString()); - BigDecimal result = quotaManagerImplSpy.aggregateQuotaTariffsValues(usageVoMock, tariffs, false, jsInterpreterMock, ""); + Map result = quotaManagerImplSpy.aggregateQuotaTariffsValues(usageVoMock, tariffs, false, jsInterpreterMock, ""); - Assert.assertEquals(BigDecimal.ZERO, result); + Assert.assertTrue(result.isEmpty()); } @Test - public void aggregateQuotaTariffsValuesTestTariffsIsEmptyReturnZero() { - BigDecimal result = quotaManagerImplSpy.aggregateQuotaTariffsValues(usageVoMock, new ArrayList<>(), false, jsInterpreterMock, ""); + public void aggregateQuotaTariffsValuesTestTariffsIsEmptyReturnEmptyMap() { + Map result = quotaManagerImplSpy.aggregateQuotaTariffsValues(usageVoMock, new ArrayList<>(), false, jsInterpreterMock, ""); - Assert.assertEquals(BigDecimal.ZERO, result); + Assert.assertTrue(result.isEmpty()); } @Test - public void aggregateQuotaTariffsValuesTestTariffsAreInPeriodToBeAppliedReturnAggregation() { + public void aggregateQuotaTariffsValuesTestTariffsAreInPeriodToBeAppliedReturnTariffsWithTheirValues() { List tariffs = createTariffList(); Mockito.doReturn(true, false, true).when(quotaManagerImplSpy).isQuotaTariffInPeriodToBeApplied(Mockito.any(), Mockito.any(), Mockito.anyString()); Mockito.doReturn(BigDecimal.TEN).when(quotaManagerImplSpy).getQuotaTariffValueToBeApplied(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()); - BigDecimal result = quotaManagerImplSpy.aggregateQuotaTariffsValues(usageVoMock, tariffs, false, jsInterpreterMock, ""); + Map result = quotaManagerImplSpy.aggregateQuotaTariffsValues(usageVoMock, tariffs, false, jsInterpreterMock, ""); - Assert.assertEquals(BigDecimal.TEN.multiply(new BigDecimal(2)), result); + Assert.assertEquals(2, result.size()); + Assert.assertTrue(result.values().stream().allMatch(value -> value.equals(BigDecimal.TEN))); } @Test public void persistUsagesAndQuotaUsagesAndRetrievePersistedQuotaUsagesTestReturnOnlyPersistedQuotaUsageVo() { - List> listPair = new ArrayList<>(); + Map>> mapUsageQuotaUsage = new LinkedHashMap<>(); QuotaUsageVO quotaUsageVoMock1 = Mockito.mock(QuotaUsageVO.class); QuotaUsageVO quotaUsageVoMock2 = Mockito.mock(QuotaUsageVO.class); - listPair.add(new Pair<>(new UsageVO(), quotaUsageVoMock1)); - listPair.add(new Pair<>(new UsageVO(), null)); - listPair.add(new Pair<>(new UsageVO(), quotaUsageVoMock2)); + mapUsageQuotaUsage.put(new UsageVO(), new Pair<>(quotaUsageVoMock1, new ArrayList<>())); + mapUsageQuotaUsage.put(new UsageVO(), null); + mapUsageQuotaUsage.put(new UsageVO(), new Pair<>(quotaUsageVoMock2, new ArrayList<>())); Mockito.doReturn(null).when(usageDaoMock).persistUsage(Mockito.any()); Mockito.doReturn(null).when(quotaUsageDaoMock).persistQuotaUsage(Mockito.any()); + Mockito.doNothing().when(quotaManagerImplSpy).persistQuotaTariffUsages(Mockito.any(), Mockito.any()); - List result = quotaManagerImplSpy.persistUsagesAndQuotaUsagesAndRetrievePersistedQuotaUsages(listPair); + List result = quotaManagerImplSpy.persistUsagesAndQuotaUsagesAndRetrievePersistedQuotaUsages(mapUsageQuotaUsage); Assert.assertEquals(2, result.size()); Assert.assertEquals(quotaUsageVoMock1, result.get(0)); @@ -551,4 +559,42 @@ private static List createLastAppliedTariffsPresetVariableList(int numbe return lastTariffs; } + @Test + public void createQuotaTariffUsageTestTariffValueIsNotZeroReturnDetailedVo() { + Mockito.doReturn(1).when(usageVoMock).getUsageType(); + Mockito.doReturn(BigDecimal.TEN).when(quotaManagerImplSpy).getUsageValueAccordingToUsageUnitType(Mockito.any(), Mockito.any(), Mockito.any()); + Mockito.doReturn(1L).when(quotaTariffVoMock).getId(); + + QuotaTariffUsageVO result = quotaManagerImplSpy.createQuotaTariffUsage(usageVoMock, quotaTariffVoMock, BigDecimal.TEN); + + Assert.assertEquals(1L, result.getTariffId().longValue()); + Assert.assertEquals(BigDecimal.TEN, result.getQuotaUsed()); + } + + @Test + public void persistQuotaTariffUsagesTestZeroQuotaTariffUsagesZeroPersisted() { + List quotaTariffUsages = new ArrayList<>(); + + quotaManagerImplSpy.persistQuotaTariffUsages(quotaTariffUsages, 1L); + + Mockito.verify(quotaTariffUsageDaoMock, Mockito.never()).persistQuotaTariffUsage(Mockito.any()); + } + + @Test + public void persistQuotaTariffUsagesTestTwoQuotaUsageDetailsTwoPersisted() { + List quotaUsageDetails = new ArrayList<>(); + QuotaTariffUsageVO quotaUsageDetailVoMock1 = Mockito.mock(QuotaTariffUsageVO.class); + QuotaTariffUsageVO quotaUsageDetailVoMock2 = Mockito.mock(QuotaTariffUsageVO.class); + + quotaUsageDetails.add(quotaUsageDetailVoMock1); + quotaUsageDetails.add(quotaUsageDetailVoMock2); + + Mockito.doNothing().when(quotaTariffUsageDaoMock).persistQuotaTariffUsage(Mockito.any()); + + quotaManagerImplSpy.persistQuotaTariffUsages(quotaUsageDetails, 1L); + + Mockito.verify(quotaTariffUsageDaoMock, Mockito.times(2)).persistQuotaTariffUsage(Mockito.any()); + } + + } diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaResourceStatementCmd.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaResourceStatementCmd.java new file mode 100644 index 000000000000..00dcb8fa5752 --- /dev/null +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaResourceStatementCmd.java @@ -0,0 +1,118 @@ +//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 org.apache.cloudstack.api.command; + +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.response.AccountResponse; +import org.apache.cloudstack.api.response.DomainResponse; +import org.apache.cloudstack.api.response.ProjectResponse; +import org.apache.cloudstack.api.response.QuotaResponseBuilder; +import org.apache.cloudstack.api.response.QuotaResourceStatementResponse; +import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.ObjectUtils; + +import javax.inject.Inject; + +import java.util.Date; + +@APICommand(name = "quotaResourceStatement", responseObject = QuotaResourceStatementResponse.class, since = "4.23.0.0", + description = "Generates a detailed Quota statement for a specific resource.", requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false) +public class QuotaResourceStatementCmd extends BaseCmd { + + @Parameter(name = ApiConstants.ID, type = CommandType.STRING, required = true, description = "ID of the resource.") + private String id; + + @Parameter(name = ApiConstants.ACCOUNT_ID, type = CommandType.UUID, entityType = AccountResponse.class, + description = "Generate the statement for this Account. A resource may have belonged to different owners at distinct points in time, " + + "so this parameter can be used to only consider the period for which it belonged to a specific Account.") + private Long accountId; + + @Parameter(name = ApiConstants.PROJECT_ID, type = CommandType.UUID, entityType = ProjectResponse.class, + description = "Generate the statement for this Project. A resource may have belonged to different owners at distinct points in time, " + + "so this parameter can be used to only consider the period for which it belonged to a specific Project.") + private Long projectId; + + @Parameter(name = ApiConstants.DOMAIN_ID, type = CommandType.UUID, entityType = DomainResponse.class, + description = "ID of the Domain for which the Quota statement will be generated.") + private Long domainId; + + @Parameter(name = ApiConstants.USAGE_TYPE, type = CommandType.INTEGER, required = true, description = "Usage type of the Quota usage.") + private Integer usageType; + + @Parameter(name = ApiConstants.START_DATE, type = CommandType.DATE, required = true, description = "Start date for the query. " + + ApiConstants.PARAMETER_DESCRIPTION_START_DATE_POSSIBLE_FORMATS) + private Date startDate; + + @Parameter(name = ApiConstants.END_DATE, type = CommandType.DATE, required = true, description = "End date for the query. " + + ApiConstants.PARAMETER_DESCRIPTION_END_DATE_POSSIBLE_FORMATS) + private Date endDate; + + @Parameter(name = ApiConstants.IS_RECURSIVE, type = CommandType.BOOLEAN, description = "Whether to include usage records from children of the filtered domain. " + + " Defaults to false.") + private Boolean recursive; + + @Inject + QuotaResponseBuilder responseBuilder; + + @Override + public void execute() { + QuotaResourceStatementResponse response = responseBuilder.createQuotaResourceStatement(this); + response.setResponseName(getCommandName()); + setResponseObject(response); + } + + @Override + public long getEntityOwnerId() { + if (ObjectUtils.allNull(accountId, projectId)) { + return -1; + } + return _accountService.finalizeAccountId(accountId, null, null, projectId); + } + + public String getId() { + return id; + } + + public Integer getUsageType() { + return usageType; + } + + public Date getStartDate() { + return startDate; + } + + public Date getEndDate() { + return endDate; + } + + public Long getAccountId() { + return accountId; + } + + public Long getDomainId() { + return domainId; + } + + public boolean isRecursive() { + return BooleanUtils.isTrue(recursive); + } + +} diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaStatementCmd.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaStatementCmd.java index bfe26a9f4250..f3a27ea58718 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaStatementCmd.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaStatementCmd.java @@ -20,7 +20,6 @@ import javax.inject.Inject; -import org.apache.cloudstack.api.ACL; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.BaseCmd; @@ -32,18 +31,17 @@ import org.apache.cloudstack.api.response.QuotaStatementItemResponse; import org.apache.cloudstack.api.response.QuotaStatementResponse; +import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.ObjectUtils; @APICommand(name = "quotaStatement", responseObject = QuotaStatementItemResponse.class, description = "Create a Quota statement for the provided Account, Project, or Domain.", since = "4.7.0", requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, httpMethod = "GET") public class QuotaStatementCmd extends BaseCmd { - @ACL @Parameter(name = ApiConstants.ACCOUNT, type = CommandType.STRING, description = "Name of the Account for which the Quota statement will be generated. Deprecated, please use accountid instead.") private String accountName; - @ACL @Parameter(name = ApiConstants.DOMAIN_ID, type = CommandType.UUID, entityType = DomainResponse.class, description = "ID of the Domain for which the Quota statement will be generated. May be used individually or with account.") private Long domainId; @@ -60,16 +58,18 @@ public class QuotaStatementCmd extends BaseCmd { description = "Consider only Quota usage records for the specified usage type in the statement.") private Integer usageType; - @ACL @Parameter(name = ApiConstants.ACCOUNT_ID, type = CommandType.UUID, entityType = AccountResponse.class, description = "ID of the Account for which the Quota statement will be generated. Can not be specified with projectid.") private Long accountId; - @ACL @Parameter(name = ApiConstants.PROJECT_ID, type = CommandType.UUID, entityType = ProjectResponse.class, description = "ID of the Project for which the Quota statement will be generated. Can not be specified with accountid.", since = "4.23.0") private Long projectId; + @Parameter(name = ApiConstants.IS_RECURSIVE, type = CommandType.BOOLEAN, description = "Whether to include usage records from children of the filtered domain. " + + " Defaults to false.") + private Boolean recursive; + @Parameter(name = ApiConstants.SHOW_RESOURCES, type = CommandType.BOOLEAN, description = "List the resources of each Quota type in the period.", since = "4.23.0") private boolean showResources; @@ -152,4 +152,9 @@ public void execute() { response.setResponseName(getCommandName()); setResponseObject(response); } + + public boolean isRecursive() { + return BooleanUtils.isTrue(recursive); + } + } diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResourceStatementItemResponse.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResourceStatementItemResponse.java new file mode 100644 index 000000000000..829363e1a405 --- /dev/null +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResourceStatementItemResponse.java @@ -0,0 +1,82 @@ +//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 org.apache.cloudstack.api.response; + +import java.math.BigDecimal; +import java.util.Date; + +import com.google.gson.annotations.SerializedName; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseResponse; + +import com.cloud.serializer.Param; + +public class QuotaResourceStatementItemResponse extends BaseResponse { + + @SerializedName(ApiConstants.TARIFF_ID) + @Param(description = "ID of the tariff.") + private String tariffId; + + @SerializedName(ApiConstants.TARIFF_NAME) + @Param(description = "Name of the tariff.") + private String tariffName; + + @SerializedName(ApiConstants.QUOTA_CONSUMED) + @Param(description = "Amount of quota used.") + private BigDecimal quotaUsed; + + @SerializedName(ApiConstants.START_DATE) + @Param(description = "Item's start date.") + private Date startDate; + + @SerializedName(ApiConstants.END_DATE) + @Param(description = "Item's end date.") + private Date endDate; + + @SerializedName(ApiConstants.ACCOUNT_ID) + @Param(description = "UUID of the resource's owner.") + private String accountId; + + public QuotaResourceStatementItemResponse() { + super("quotaresourcestatementitem"); + } + + public void setTariffId(String tariffId) { + this.tariffId = tariffId; + } + + public void setTariffName(String tariffName) { + this.tariffName = tariffName; + } + + public void setQuotaUsed(BigDecimal quotaUsed) { + this.quotaUsed = quotaUsed; + } + + public void setStartDate(Date startDate) { + this.startDate = startDate; + } + + public void setEndDate(Date endDate) { + this.endDate = endDate; + } + + public void setAccountId(String accountId) { + this.accountId = accountId; + } +} \ No newline at end of file diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResourceStatementResponse.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResourceStatementResponse.java new file mode 100644 index 000000000000..ce3ab1f177ba --- /dev/null +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResourceStatementResponse.java @@ -0,0 +1,66 @@ +//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 org.apache.cloudstack.api.response; + +import com.cloud.serializer.Param; +import com.google.gson.annotations.SerializedName; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseResponse; + +import java.math.BigDecimal; +import java.util.List; + +public class QuotaResourceStatementResponse extends BaseResponse { + + @SerializedName(ApiConstants.USAGE_NAME) + @Param(description = "Name of the usage type.") + private String usageName; + + @SerializedName(ApiConstants.UNIT) + @Param(description = "Unit of the usage type.") + private String unit; + + @SerializedName(ApiConstants.ITEMS) + @Param(description = "List of Quota tariff usages.", responseObject = QuotaResourceStatementItemResponse.class) + private List items; + + @SerializedName(ApiConstants.TOTAL_QUOTA) + @Param(description = "Total amount of quota used.") + private BigDecimal totalQuotaUsed; + + public QuotaResourceStatementResponse() { + super("quotaresourcestatement"); + } + + public void setUsageName(String usageName) { + this.usageName = usageName; + } + + public void setUnit(String unit) { + this.unit = unit; + } + + public void setQuotaUsageDetails(List items) { + this.items = items; + } + + public void setTotalQuotaUsed(BigDecimal totalQuotaUsed) { + this.totalQuotaUsed = totalQuotaUsed; + } + +} diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilder.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilder.java index bde905c487b7..07f8935ab302 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilder.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilder.java @@ -23,6 +23,7 @@ import org.apache.cloudstack.api.command.QuotaEmailTemplateListCmd; import org.apache.cloudstack.api.command.QuotaEmailTemplateUpdateCmd; import org.apache.cloudstack.api.command.QuotaPresetVariablesListCmd; +import org.apache.cloudstack.api.command.QuotaResourceStatementCmd; import org.apache.cloudstack.api.command.QuotaStatementCmd; import org.apache.cloudstack.api.command.QuotaSummaryCmd; import org.apache.cloudstack.api.command.QuotaTariffCreateCmd; @@ -88,4 +89,6 @@ public interface QuotaResponseBuilder { Pair, Integer> createQuotaCreditsListResponse(QuotaCreditsListCmd cmd); QuotaValidateActivationRuleResponse validateActivationRule(QuotaValidateActivationRuleCmd cmd); + + QuotaResourceStatementResponse createQuotaResourceStatement(QuotaResourceStatementCmd cmd); } 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..d65f5156a775 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 @@ -69,11 +69,14 @@ import com.cloud.user.UserVO; import com.cloud.utils.DateUtil; import com.cloud.utils.Pair; +import com.cloud.utils.db.EntityManager; import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.vm.VMInstanceVO; import com.cloud.vm.dao.VMInstanceDao; +import org.apache.cloudstack.acl.ControlledEntity; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.InternalIdentity; import org.apache.cloudstack.api.ServerApiException; import org.apache.cloudstack.api.command.QuotaBalanceCmd; import org.apache.cloudstack.api.command.QuotaConfigureEmailCmd; @@ -81,6 +84,7 @@ import org.apache.cloudstack.api.command.QuotaEmailTemplateListCmd; import org.apache.cloudstack.api.command.QuotaEmailTemplateUpdateCmd; import org.apache.cloudstack.api.command.QuotaPresetVariablesListCmd; +import org.apache.cloudstack.api.command.QuotaResourceStatementCmd; import org.apache.cloudstack.api.command.QuotaStatementCmd; import org.apache.cloudstack.api.command.QuotaSummaryCmd; import org.apache.cloudstack.api.command.QuotaTariffCreateCmd; @@ -110,16 +114,20 @@ import org.apache.cloudstack.quota.dao.QuotaEmailTemplatesDao; import org.apache.cloudstack.quota.dao.QuotaSummaryDao; import org.apache.cloudstack.quota.dao.QuotaTariffDao; +import org.apache.cloudstack.quota.dao.QuotaTariffUsageDao; import org.apache.cloudstack.quota.dao.QuotaUsageDao; +import org.apache.cloudstack.quota.dao.QuotaUsageJoinDao; import org.apache.cloudstack.quota.vo.QuotaAccountVO; import org.apache.cloudstack.quota.vo.QuotaBalanceVO; import org.apache.cloudstack.quota.vo.QuotaCreditsVO; import org.apache.cloudstack.quota.vo.QuotaEmailConfigurationVO; import org.apache.cloudstack.quota.vo.QuotaEmailTemplatesVO; import org.apache.cloudstack.quota.vo.QuotaSummaryVO; +import org.apache.cloudstack.quota.vo.QuotaTariffUsageVO; import org.apache.cloudstack.quota.vo.QuotaTariffVO; import org.apache.cloudstack.quota.vo.QuotaUsageJoinVO; import org.apache.cloudstack.quota.vo.QuotaUsageResourceVO; +import org.apache.cloudstack.usage.UsageTypes; import org.apache.cloudstack.utils.jsinterpreter.JsInterpreter; import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; import org.apache.commons.collections4.CollectionUtils; @@ -184,7 +192,12 @@ public class QuotaResponseBuilderImpl implements QuotaResponseBuilder { private VMTemplateDao vmTemplateDao; @Inject private VolumeDao volumeDao; - + @Inject + private QuotaUsageJoinDao quotaUsageJoinDao; + @Inject + private QuotaTariffUsageDao quotaTariffUsageDao; + @Inject + private EntityManager entityMgr; private final Class[] assignableClasses = {GenericPresetVariable.class, ComputingResources.class, ResourceCounting.class}; @@ -420,9 +433,9 @@ public int compare(QuotaBalanceVO o1, QuotaBalanceVO o2) { @Override public QuotaStatementResponse createQuotaStatementResponse(QuotaStatementCmd cmd) { - Long accountId = getAccountIdForQuotaStatement(cmd); - Long domainId = getDomainIdForQuotaStatement(cmd, accountId); - List quotaUsages = _quotaService.getQuotaUsage(accountId, null, domainId, cmd.getUsageType(), cmd.getStartDate(), cmd.getEndDate()); + Long accountId = getAccountIdForQuotaStatement(cmd.getEntityOwnerId(), null); + Pair> baseDomainAndFilteredDomains = getDomainIdsForQuotaStatement(accountId, cmd.getDomainId(), cmd.isRecursive()); + List quotaUsages = _quotaService.getQuotaUsage(accountId, null, baseDomainAndFilteredDomains.second(), cmd.getUsageType(), cmd.getStartDate(), cmd.getEndDate()); logger.debug("Creating quota statement from [{}] usage records for parameters [{}].", quotaUsages.size(), ReflectionToStringBuilderUtils.reflectOnlySelectedFields(cmd, "accountName", "accountId", "projectId", "domainId", "startDate", "endDate", "usageType", "showResources")); @@ -445,52 +458,16 @@ public QuotaStatementResponse createQuotaStatementResponse(QuotaStatementCmd cmd Account account = _accountDao.findByIdIncludingRemoved(accountId); statement.setAccountId(account.getUuid()); statement.setAccountName(account.getAccountName()); - domainId = account.getDomainId(); } - if (domainId != null) { - DomainVO domain = domainDao.findByIdIncludingRemoved(domainId); + Long baseDomainId = baseDomainAndFilteredDomains.first(); + if (baseDomainId != null) { + DomainVO domain = domainDao.findByIdIncludingRemoved(baseDomainId); statement.setDomainId(domain.getUuid()); } return statement; } - protected Long getAccountIdForQuotaStatement(QuotaStatementCmd cmd) { - if (Account.Type.NORMAL.equals(CallContext.current().getCallingAccount().getType())) { - logger.debug("Limiting the Quota statement for the calling Account, as they are a User Account."); - return CallContext.current().getCallingAccountId(); - } - - long accountId = cmd.getEntityOwnerId(); - if (accountId != -1) { - return accountId; - } - - if (cmd.getDomainId() == null) { - logger.debug("Limiting the Quota statement for the calling Account, as 'domainid' was not informed."); - return CallContext.current().getCallingAccountId(); - } - - logger.debug("Allowing admin/domain admin to generate the Quota statement for the provided Domain."); - return null; - } - - protected Long getDomainIdForQuotaStatement(QuotaStatementCmd cmd, Long accountId) { - if (accountId != null) { - logger.debug("Quota statement is already limited to Account [{}].", accountId); - Account account = _accountDao.findByIdIncludingRemoved(accountId); - return account.getDomainId(); - } - - Long domainId = cmd.getDomainId(); - if (domainId != null) { - return domainId; - } - - logger.debug("Limiting the Quota statement for the calling Account's Domain."); - return CallContext.current().getCallingAccount().getDomainId(); - } - protected void createDummyRecordForEachQuotaTypeIfUsageTypeIsNotInformed(List quotaUsages, Integer usageType) { if (usageType != null) { logger.debug("As the usage type [{}] was informed as parameter of the API quotaStatement, we will not create dummy records.", usageType); @@ -1264,6 +1241,185 @@ public QuotaValidateActivationRuleResponse validateActivationRule(QuotaValidateA return createValidateActivationRuleResponse(activationRule, quotaName, false, message); } + @Override + public QuotaResourceStatementResponse createQuotaResourceStatement(QuotaResourceStatementCmd cmd) { + String resourceUuid = cmd.getId(); + Integer usageType = cmd.getUsageType(); + Date startDate = cmd.getStartDate(); + Date endDate = cmd.getEndDate(); + + if (startDate.after(endDate)) { + throw new InvalidParameterValueException(String.format("The start date [%s] must be before the end date [%s].", startDate, endDate)); + } + + InternalIdentity resource = retrieveResource(resourceUuid, usageType); + if (resource == null) { + logger.error("Could not find resource [{}] of type [{}]. Returning an empty list.", resourceUuid, usageType); + return createQuotaResourceStatementResponse(resourceUuid, usageType, new ArrayList<>(), new BigDecimal(0)); + } + + long id = resource.getId(); + + Long currentOwnerId = null; + if (resource instanceof ControlledEntity) { + currentOwnerId = ((ControlledEntity) resource).getAccountId(); + } + Long accountId = getAccountIdForQuotaStatement(cmd.getEntityOwnerId(), currentOwnerId); + Pair> baseDomainAndFilteredDomains = getDomainIdsForQuotaStatement(accountId, cmd.getDomainId(), cmd.isRecursive()); + + Long resourceId = null; + Long networkId = null; + Long offeringId = null; + + switch (usageType) { + case UsageTypes.NETWORK_OFFERING: + case UsageTypes.BACKUP: + offeringId = id; + break; + case UsageTypes.NETWORK_BYTES_RECEIVED: + case UsageTypes.NETWORK_BYTES_SENT: + networkId = id; + break; + default: + resourceId = id; + } + + logger.debug("Attempting to find quota usages with parameters usage type [{}], usage id [{}], network id [{}], offering id [{}], and between [{}] and [{}].", + usageType, resourceId, networkId, offeringId, startDate, endDate); + + List quotaUsageJoinList = quotaUsageJoinDao.findQuotaUsage(accountId, baseDomainAndFilteredDomains.second(), usageType, resourceId, networkId, offeringId, startDate, endDate, null); + + logger.debug("Found [{}] quota usages using as parameter usage type [{}], usage id [{}], network id [{}], offering id [{}], and between [{}] and [{}].", + quotaUsageJoinList.size(), usageType, resourceId, networkId, offeringId, startDate, endDate); + + List quotaResourceStatementItemResponseList = new ArrayList<>(); + BigDecimal totalQuotaUsed = new BigDecimal(0); + List quotaTariffs = _quotaTariffDao.listQuotaTariffs(null, null, usageType, null, null, true, null, null).first(); + + for (QuotaUsageJoinVO quotaUsageJoin : quotaUsageJoinList) { + Account account = _accountMgr.getAccount(quotaUsageJoin.getAccountId()); + String accountUuid = account.getUuid(); + + List quotaTariffUsageList = quotaTariffUsageDao.listQuotaTariffUsages(quotaUsageJoin.getId()); + logger.debug("Found [{}] quota tariff usages associated to the quota usage [{}] of resource [{}] and type [{}] between [{}] and [{}].", + quotaTariffUsageList.size(), quotaUsageJoin, resourceUuid, usageType, startDate, endDate); + for (QuotaTariffUsageVO quotaTariffUsage: quotaTariffUsageList) { + quotaResourceStatementItemResponseList.add(createQuotaResourceStatementItemResponse(quotaTariffUsage, quotaTariffs, quotaUsageJoin.getStartDate(), + quotaUsageJoin.getEndDate(), accountUuid)); + totalQuotaUsed = totalQuotaUsed.add(quotaTariffUsage.getQuotaUsed()); + } + } + logger.debug("The total quota used of type [{}] between [{}] and [{}] for the resource [{}] was [{}].", usageType, startDate, endDate, resourceUuid, totalQuotaUsed); + + return createQuotaResourceStatementResponse(resourceUuid, usageType, quotaResourceStatementItemResponseList, totalQuotaUsed); + } + + protected Long getAccountIdForQuotaStatement(long providedAccountId, Long fallbackAccountId) { + Account caller = CallContext.current().getCallingAccount(); + + if (providedAccountId != -1L) { + Account account = _accountDao.findByIdIncludingRemoved(providedAccountId); + _accountMgr.checkAccess(caller, null, false, account); + logger.debug("Limiting the Quota resource statement for the provided Account [{}].", providedAccountId); + return providedAccountId; + } + + Account.Type callerType = caller.getType(); + if (Account.Type.ADMIN.equals(callerType) || Account.Type.DOMAIN_ADMIN.equals(callerType)) { + logger.debug("Not limiting the Quota resource statement for a specific Account, as no specific Account was provided and the caller is either an admin or domain admin."); + return null; + } + + if (fallbackAccountId != null) { + Account fallbackAccount = _accountDao.findByIdIncludingRemoved(fallbackAccountId); + _accountMgr.checkAccess(caller, null, false, fallbackAccount); + logger.debug("Limiting the Quota statement for the fallback Account [{}], as no specific Account was provided.", fallbackAccountId); + return fallbackAccountId; + } + + logger.debug("Limiting the Quota resource statement for the calling account, as no specific Account was provided and no fallback account was provided."); + return caller.getAccountId(); + } + + protected Pair> getDomainIdsForQuotaStatement(Long finalAccountId, Long providedDomainId, boolean isRecursive) { + if (finalAccountId != null) { + // Access to the provided account has already been validated + logger.debug("Not limiting the Quota statement for a specific Domain, as we are already limiting by Account."); + return new Pair<>(null, null); + } + + // User accounts will have already been limited to themselves + Account caller = CallContext.current().getCallingAccount(); + Long domainId = providedDomainId; + + if (domainId != null) { + Domain domain = domainDao.findByIdIncludingRemoved(domainId); + _accountMgr.checkAccess(caller, domain); + logger.debug("Limiting the Quota statement for the provided Domain [{}].", domainId); + } + if (domainId == null) { + domainId = caller.getDomainId(); + logger.debug("Limiting the Quota statement for the caller's Domain [{}], as no 'domainid' was provided.", domainId); + } + + if (isRecursive) { + logger.debug("Allowing the Quota statement for the Domain's children, as the 'isrecursive' parameter was provided."); + return new Pair<>(domainId, domainDao.getDomainAndChildrenIds(domainId)); + } + + return new Pair<>(domainId, List.of(domainId)); + } + + protected InternalIdentity retrieveResource(String resourceUuid, Integer usageType) { + Class clazz = QuotaTypes.getClazz(usageType); + if (clazz == null) { + throw new InvalidParameterValueException(String.format("Invalid usage type [%s] provided.", usageType)); + } + + logger.debug("Attempting to find a resource with ID [{}] and of type [{}].", resourceUuid, usageType); + Object object = entityMgr.findByUuidIncludingRemoved(clazz, resourceUuid); + if (object == null) { + return null; + } + return (InternalIdentity) object; + } + + protected QuotaResourceStatementItemResponse createQuotaResourceStatementItemResponse(QuotaTariffUsageVO quotaTariffUsage, List quotaTariffs, + Date startDate, Date endDate, String accountUuid) { + logger.trace("Creating quota resource statement item associated to quota tariff usage [{}].", quotaTariffUsage); + QuotaResourceStatementItemResponse quotaResourceStatementItemResponse = new QuotaResourceStatementItemResponse(); + + QuotaTariffVO quotaTariff = quotaTariffs.stream().filter(quotaTariffVO -> quotaTariffUsage.getTariffId().equals(quotaTariffVO.getId())).findAny().orElse(null); + + quotaResourceStatementItemResponse.setQuotaUsed(quotaTariffUsage.getQuotaUsed()); + quotaResourceStatementItemResponse.setStartDate(startDate); + quotaResourceStatementItemResponse.setEndDate(endDate); + quotaResourceStatementItemResponse.setAccountId(accountUuid); + if (quotaTariff != null) { + logger.trace("Quota usage details item will be associated to the quota tariff [{}].", quotaTariff); + quotaResourceStatementItemResponse.setTariffId(quotaTariff.getUuid()); + quotaResourceStatementItemResponse.setTariffName(quotaTariff.getName()); + } + + return quotaResourceStatementItemResponse; + } + + protected QuotaResourceStatementResponse createQuotaResourceStatementResponse(String resourceUuid, Integer usageType, + List quotaUsageDetailsItems, BigDecimal totalQuotaUsed) { + logger.trace("Creating quota usage details list response associated to the resource of UUID [{}], with an usage type of [{}], [{}] quota" + + " usage details items, and a total quota used of [{}].", resourceUuid, usageType, quotaUsageDetailsItems.size(), totalQuotaUsed); + QuotaResourceStatementResponse quotaResourceStatementResponse = new QuotaResourceStatementResponse(); + + QuotaTypes quotaType = QuotaTypes.getQuotaType(usageType); + quotaResourceStatementResponse.setUsageName(quotaType.getQuotaName()); + quotaResourceStatementResponse.setUnit(quotaType.getQuotaUnit()); + + quotaResourceStatementResponse.setQuotaUsageDetails(quotaUsageDetailsItems); + quotaResourceStatementResponse.setTotalQuotaUsed(totalQuotaUsed); + + return quotaResourceStatementResponse; + } + /** * Checks whether script variables are compatible with the usage type. First, we remove all script variables that correspond to the script's usage type variables. * Then, returns true if none of the remaining script variables match any usage types variables, and false otherwise. diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaStatementResponse.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaStatementResponse.java index 81cb1011182d..c30780582d36 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaStatementResponse.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaStatementResponse.java @@ -31,11 +31,11 @@ public class QuotaStatementResponse extends BaseResponse { @Param(description = "ID of the Account.") private String accountId; - @SerializedName(ApiConstants.ACCOUNT) + @SerializedName(ApiConstants.ACCOUNT_NAME) @Param(description = "Name of the Account.") private String accountName; - @SerializedName(ApiConstants.DOMAIN) + @SerializedName(ApiConstants.DOMAIN_ID) @Param(description = "ID of the Domain.") private String domainId; diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/quota/QuotaService.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/quota/QuotaService.java index 78acfc11682e..39ddd29cd99a 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/quota/QuotaService.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/quota/QuotaService.java @@ -28,7 +28,7 @@ public interface QuotaService extends PluggableService { - List getQuotaUsage(Long accountId, String accountName, Long domainId, Integer usageType, Date startDate, Date endDate); + List getQuotaUsage(Long accountId, String accountName, List domainIds, Integer usageType, Date startDate, Date endDate); List findQuotaBalanceVO(Long accountId, String accountName, Long domainId, Date startDate, Date endDate); diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/quota/QuotaServiceImpl.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/quota/QuotaServiceImpl.java index a0ba2fbc751d..a4e9d16313d7 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/quota/QuotaServiceImpl.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/quota/QuotaServiceImpl.java @@ -37,6 +37,7 @@ import org.apache.cloudstack.api.command.QuotaEnabledCmd; import org.apache.cloudstack.api.command.QuotaListEmailConfigurationCmd; import org.apache.cloudstack.api.command.QuotaPresetVariablesListCmd; +import org.apache.cloudstack.api.command.QuotaResourceStatementCmd; import org.apache.cloudstack.api.command.QuotaStatementCmd; import org.apache.cloudstack.api.command.QuotaSummaryCmd; import org.apache.cloudstack.api.command.QuotaTariffCreateCmd; @@ -131,6 +132,7 @@ public List> getCommands() { cmdList.add(QuotaListEmailConfigurationCmd.class); cmdList.add(QuotaPresetVariablesListCmd.class); cmdList.add(QuotaValidateActivationRuleCmd.class); + cmdList.add(QuotaResourceStatementCmd.class); return cmdList; } @@ -213,15 +215,15 @@ public List findQuotaBalanceVO(Long accountId, String accountNam } @Override - public List getQuotaUsage(Long accountId, String accountName, Long domainId, Integer usageType, Date startDate, Date endDate) { + public List getQuotaUsage(Long accountId, String accountName, List domainIds, Integer usageType, Date startDate, Date endDate) { if (startDate.after(endDate)) { throw new InvalidParameterValueException("Incorrect Date Range. Start date: " + startDate + " is after end date:" + endDate); } - logger.debug("Getting quota records of type [{}] for account [{}] in domain [{}], between [{}] and [{}].", - usageType, accountId, domainId, startDate, endDate); + logger.debug("Getting quota records of type [{}] for account [{}] in domains [{}], between [{}] and [{}].", + usageType, accountId, domainIds, startDate, endDate); - return quotaUsageJoinDao.findQuotaUsage(accountId, domainId, usageType, null, null, null, startDate, endDate, null); + return quotaUsageJoinDao.findQuotaUsage(accountId, domainIds, usageType, null, null, null, startDate, endDate, null); } @Override diff --git a/plugins/database/quota/src/test/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImplTest.java b/plugins/database/quota/src/test/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImplTest.java index 81b4992082d9..0792110316fe 100644 --- a/plugins/database/quota/src/test/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImplTest.java +++ b/plugins/database/quota/src/test/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImplTest.java @@ -44,7 +44,6 @@ import org.apache.cloudstack.api.command.QuotaCreditsListCmd; import org.apache.cloudstack.api.command.QuotaEmailTemplateListCmd; import org.apache.cloudstack.api.command.QuotaEmailTemplateUpdateCmd; -import org.apache.cloudstack.api.command.QuotaStatementCmd; import org.apache.cloudstack.api.command.QuotaSummaryCmd; import org.apache.cloudstack.api.command.QuotaValidateActivationRuleCmd; import org.apache.cloudstack.context.CallContext; @@ -155,7 +154,7 @@ public class QuotaResponseBuilderImplTest extends TestCase { Date date = new Date(); @Mock - Account accountMock; + AccountVO accountMock; @Mock DomainVO domainVoMock; @@ -1010,107 +1009,130 @@ public void setStatementItemResourcesTestDoNotShowResourcesDoNothing() { } @Test - public void getAccountIdForQuotaStatementTestLimitsToCallingAccountForNormalUser() { - QuotaStatementCmd cmd = Mockito.mock(QuotaStatementCmd.class); + public void getAccountIdForQuotaStatementTestReturnsProvidedAccount() { + long providedAccountId = 200L; - Mockito.doReturn(accountMock).when(callContextMock).getCallingAccount(); - Mockito.doReturn(Account.Type.NORMAL).when(accountMock).getType(); + Mockito.when(accountDaoMock.findByIdIncludingRemoved(providedAccountId)).thenReturn(accountMock); + Mockito.doNothing().when(accountManagerMock).checkAccess(callerAccountMock, null, false, accountMock); - try (MockedStatic callContextMocked = Mockito.mockStatic(CallContext.class)) { - callContextMocked.when(CallContext::current).thenReturn(callContextMock); - - Long result = quotaResponseBuilderSpy.getAccountIdForQuotaStatement(cmd); + long result = quotaResponseBuilderSpy.getAccountIdForQuotaStatement(providedAccountId, null); - Assert.assertEquals(Long.valueOf(callerAccountMock.getAccountId()), result); - } + Assert.assertEquals(200L, result); + Mockito.verify(accountManagerMock).checkAccess(callerAccountMock, null, false, accountMock); } @Test - public void getAccountIdForQuotaStatementTestReturnsEntityOwnerIdWhenProvided() { - QuotaStatementCmd cmd = Mockito.mock(QuotaStatementCmd.class); + public void getAccountIdForQuotaStatementTestReturnsNullWhenCallerIsAdminWithoutProvidedAccount() { + Mockito.when(callerAccountMock.getType()).thenReturn(Account.Type.ADMIN); - Mockito.doReturn(42L).when(cmd).getEntityOwnerId(); + Long result = quotaResponseBuilderSpy.getAccountIdForQuotaStatement(-1L, null); - Long result = quotaResponseBuilderSpy.getAccountIdForQuotaStatement(cmd); + assertNull(result); + Mockito.verify(accountManagerMock, Mockito.never()).getAccount(Mockito.anyLong()); + } + + @Test + public void getAccountIdForQuotaStatementTestReturnsNullWhenCallerIsDomainAdminWithoutProvidedAccount() { + Mockito.when(callerAccountMock.getType()).thenReturn(Account.Type.DOMAIN_ADMIN); - Assert.assertEquals(Long.valueOf(42L), result); + Long result = quotaResponseBuilderSpy.getAccountIdForQuotaStatement(-1L, null); + + assertNull(result); + Mockito.verify(accountManagerMock, Mockito.never()).getAccount(Mockito.anyLong()); } @Test - public void getAccountIdForQuotaStatementTestLimitsToCallingAccountWhenCallerIsAdminAndDomainIsNotProvided() { - QuotaStatementCmd cmd = Mockito.mock(QuotaStatementCmd.class); + public void getAccountIdForQuotaStatementTestReturnsFallbackAccountWhenNoAccountProvidedAndCallerIsNotAdmin() { + Mockito.when(callerAccountMock.getType()).thenReturn(Account.Type.NORMAL); - Mockito.doReturn(accountMock).when(callContextMock).getCallingAccount(); - Mockito.doReturn(Account.Type.ADMIN).when(accountMock).getType(); - Mockito.doReturn(-1L).when(cmd).getEntityOwnerId(); - Mockito.doReturn(null).when(cmd).getDomainId(); + Mockito.when(accountDaoMock.findByIdIncludingRemoved(300L)).thenReturn(accountMock); + Mockito.doNothing().when(accountManagerMock).checkAccess(callerAccountMock, null, false, accountMock); - try (MockedStatic callContextMocked = Mockito.mockStatic(CallContext.class)) { - callContextMocked.when(CallContext::current).thenReturn(callContextMock); + long result = quotaResponseBuilderSpy.getAccountIdForQuotaStatement(-1L, 300L); - Long result = quotaResponseBuilderSpy.getAccountIdForQuotaStatement(cmd); + assertEquals(300L, result); + Mockito.verify(accountManagerMock).checkAccess(callerAccountMock, null, false, accountMock); + } - Assert.assertEquals(Long.valueOf(callerAccountMock.getAccountId()), result); - } + @Test + public void getAccountIdForQuotaStatementTestAccessDeniedForProvidedAccount() { + Mockito.when(accountDaoMock.findByIdIncludingRemoved(200L)).thenReturn(accountMock); + Mockito.doThrow(new PermissionDeniedException("Access denied")) + .when(accountManagerMock).checkAccess(callerAccountMock, null, false, accountMock); + + Assert.assertThrows(PermissionDeniedException.class, + () -> quotaResponseBuilderSpy.getAccountIdForQuotaStatement(200L, null)); + Mockito.verify(accountManagerMock).checkAccess(callerAccountMock, null, false, accountMock); } @Test - public void getAccountIdForQuotaStatementTestReturnsNullWhenCallerIsAdminAndDomainIsProvided() { - QuotaStatementCmd cmd = Mockito.mock(QuotaStatementCmd.class); + public void getDomainIdsForQuotaStatementTestReturnsNullPairWhenAccountIsProvided() { + Pair> result = quotaResponseBuilderSpy.getDomainIdsForQuotaStatement(100L, null, false); - Mockito.doReturn(accountMock).when(callContextMock).getCallingAccount(); - Mockito.doReturn(Account.Type.ADMIN).when(accountMock).getType(); - Mockito.doReturn(-1L).when(cmd).getEntityOwnerId(); - Mockito.doReturn(10L).when(cmd).getDomainId(); + assertNull(result.first()); + assertNull(result.second()); + Mockito.verify(domainDaoMock, Mockito.never()).findByIdIncludingRemoved(Mockito.anyLong()); + } - try (MockedStatic callContextMocked = Mockito.mockStatic(CallContext.class)) { - callContextMocked.when(CallContext::current).thenReturn(callContextMock); + @Test + public void getDomainIdsForQuotaStatementTestReturnsProvidedDomainIdNonRecursively() { + Mockito.when(domainDaoMock.findByIdIncludingRemoved(5L)).thenReturn(domainVoMock); + Mockito.doNothing().when(accountManagerMock).checkAccess(callerAccountMock, domainVoMock); - Long result = quotaResponseBuilderSpy.getAccountIdForQuotaStatement(cmd); + Pair> result = quotaResponseBuilderSpy.getDomainIdsForQuotaStatement(null, 5L, false); - Assert.assertNull(result); - } + assertEquals(5L, (long) result.first()); + assertEquals(List.of(5L), result.second()); + Mockito.verify(accountManagerMock).checkAccess(callerAccountMock, domainVoMock); } @Test - public void getDomainIdForQuotaStatementTestReturnsAccountDomainIdWhenAccountIdIsProvided() { - QuotaStatementCmd cmd = Mockito.mock(QuotaStatementCmd.class); - AccountVO account = Mockito.mock(AccountVO.class); - - Mockito.doReturn(account).when(accountDaoMock).findByIdIncludingRemoved(55L); - Mockito.doReturn(77L).when(account).getDomainId(); + public void getDomainIdsForQuotaStatementTestReturnsCallerDomainNonRecursively() { + Mockito.when(callerAccountMock.getDomainId()).thenReturn(7L); - Long result = quotaResponseBuilderSpy.getDomainIdForQuotaStatement(cmd, 55L); + Pair> result = quotaResponseBuilderSpy.getDomainIdsForQuotaStatement(null, null, false); - Assert.assertEquals(Long.valueOf(77L), result); + assertEquals(7L, (long) result.first()); + assertEquals(List.of(7L), result.second()); + Mockito.verify(domainDaoMock, Mockito.never()).findByIdIncludingRemoved(Mockito.anyLong()); } @Test - public void getDomainIdForQuotaStatementTestReturnsProvidedDomainIdWhenAccountIdIsNull() { - QuotaStatementCmd cmd = Mockito.mock(QuotaStatementCmd.class); + public void getDomainIdsForQuotaStatementTestReturnsProvidedDomainRecursively() { + List domainAndChildren = List.of(5L, 10L, 15L); + Mockito.when(domainDaoMock.findByIdIncludingRemoved(5L)).thenReturn(domainVoMock); + Mockito.when(domainDaoMock.getDomainAndChildrenIds(5L)).thenReturn(domainAndChildren); + Mockito.doNothing().when(accountManagerMock).checkAccess(callerAccountMock, domainVoMock); - Mockito.doReturn(99L).when(cmd).getDomainId(); + Pair> result = quotaResponseBuilderSpy.getDomainIdsForQuotaStatement(null, 5L, true); - Long result = quotaResponseBuilderSpy.getDomainIdForQuotaStatement(cmd, null); - - Assert.assertEquals(Long.valueOf(99L), result); + assertEquals(5L, (long) result.first()); + assertEquals(domainAndChildren, result.second()); + Mockito.verify(domainDaoMock).getDomainAndChildrenIds(5L); } @Test - public void getDomainIdForQuotaStatementTestFallsBackToCallingAccountDomainIdWhenNeitherAccountNorDomainIsProvided() { - QuotaStatementCmd cmd = Mockito.mock(QuotaStatementCmd.class); - Account account = Mockito.mock(Account.class); + public void getDomainIdsForQuotaStatementReturnsCallerDomainRecursively() { + List domainAndChildren = List.of(1L, 2L, 3L); + Mockito.when(callerAccountMock.getDomainId()).thenReturn(1L); - Mockito.doReturn(null).when(cmd).getDomainId(); - Mockito.doReturn(123L).when(account).getDomainId(); - Mockito.doReturn(account).when(callContextMock).getCallingAccount(); + Mockito.when(domainDaoMock.getDomainAndChildrenIds(1L)).thenReturn(domainAndChildren); - try (MockedStatic callContextMocked = Mockito.mockStatic(CallContext.class)) { - callContextMocked.when(CallContext::current).thenReturn(callContextMock); + Pair> result = quotaResponseBuilderSpy.getDomainIdsForQuotaStatement(null, null, true); - Long result = quotaResponseBuilderSpy.getDomainIdForQuotaStatement(cmd, null); + assertEquals(1L, (long) result.first()); + assertEquals(domainAndChildren, result.second()); + Mockito.verify(domainDaoMock).getDomainAndChildrenIds(1L); + } - Assert.assertEquals(123L, result.longValue()); - } + @Test + public void getDomainIdsForQuotaStatementTestThrowsAccessDeniedForProvidedDomain() { + Mockito.when(domainDaoMock.findByIdIncludingRemoved(5L)).thenReturn(domainVoMock); + Mockito.doThrow(new PermissionDeniedException("Access denied")) + .when(accountManagerMock).checkAccess(callerAccountMock, domainVoMock); + + Assert.assertThrows(PermissionDeniedException.class, + () -> quotaResponseBuilderSpy.getDomainIdsForQuotaStatement(null, 5L, false)); + Mockito.verify(accountManagerMock).checkAccess(callerAccountMock, domainVoMock); } } diff --git a/plugins/database/quota/src/test/java/org/apache/cloudstack/quota/QuotaServiceImplTest.java b/plugins/database/quota/src/test/java/org/apache/cloudstack/quota/QuotaServiceImplTest.java index a0fe63de8518..703d1d0b5b02 100644 --- a/plugins/database/quota/src/test/java/org/apache/cloudstack/quota/QuotaServiceImplTest.java +++ b/plugins/database/quota/src/test/java/org/apache/cloudstack/quota/QuotaServiceImplTest.java @@ -140,12 +140,12 @@ public void testFindQuotaBalanceVO() { public void testGetQuotaUsage() { final long accountId = 2L; final String accountName = "admin123"; - final long domainId = 1L; + final List domainIds = List.of(1L); final Date startDate = new DateTime().minusDays(2).toDate(); final Date endDate = new Date(); - quotaServiceImplSpy.getQuotaUsage(accountId, accountName, domainId, QuotaTypes.IP_ADDRESS, startDate, endDate); - Mockito.verify(quotaUsageJoinDaoMock, Mockito.times(1)).findQuotaUsage(Mockito.eq(accountId), Mockito.eq(domainId), Mockito.eq(QuotaTypes.IP_ADDRESS), Mockito.any(), + quotaServiceImplSpy.getQuotaUsage(accountId, accountName, domainIds, QuotaTypes.IP_ADDRESS, startDate, endDate); + Mockito.verify(quotaUsageJoinDaoMock, Mockito.times(1)).findQuotaUsage(Mockito.eq(accountId), Mockito.eq(domainIds), Mockito.eq(QuotaTypes.IP_ADDRESS), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(Date.class), Mockito.any(Date.class), Mockito.any()); } From b0ba9a11f553f515157b55663be0a219f57dde11 Mon Sep 17 00:00:00 2001 From: Fabricio Duarte Date: Mon, 25 May 2026 15:34:16 -0300 Subject: [PATCH 2/3] Fix pre-commit --- .../java/org/apache/cloudstack/quota/QuotaManagerImplTest.java | 1 - .../api/response/QuotaResourceStatementItemResponse.java | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/framework/quota/src/test/java/org/apache/cloudstack/quota/QuotaManagerImplTest.java b/framework/quota/src/test/java/org/apache/cloudstack/quota/QuotaManagerImplTest.java index 6cecddbd4dbd..1e08e7d7fc00 100644 --- a/framework/quota/src/test/java/org/apache/cloudstack/quota/QuotaManagerImplTest.java +++ b/framework/quota/src/test/java/org/apache/cloudstack/quota/QuotaManagerImplTest.java @@ -596,5 +596,4 @@ public void persistQuotaTariffUsagesTestTwoQuotaUsageDetailsTwoPersisted() { Mockito.verify(quotaTariffUsageDaoMock, Mockito.times(2)).persistQuotaTariffUsage(Mockito.any()); } - } diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResourceStatementItemResponse.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResourceStatementItemResponse.java index 829363e1a405..9f0a28389a9d 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResourceStatementItemResponse.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResourceStatementItemResponse.java @@ -79,4 +79,4 @@ public void setEndDate(Date endDate) { public void setAccountId(String accountId) { this.accountId = accountId; } -} \ No newline at end of file +} From d6e7a9f467cac989ed2d8b6777a5c85a4168fb2e Mon Sep 17 00:00:00 2001 From: Fabricio Duarte Date: Mon, 25 May 2026 18:10:39 -0300 Subject: [PATCH 3/3] Add additional unit tests --- .../QuotaResponseBuilderImplTest.java | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/plugins/database/quota/src/test/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImplTest.java b/plugins/database/quota/src/test/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImplTest.java index 0792110316fe..820396ef8ba7 100644 --- a/plugins/database/quota/src/test/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImplTest.java +++ b/plugins/database/quota/src/test/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImplTest.java @@ -38,7 +38,9 @@ import com.cloud.user.AccountManager; import com.cloud.user.UserVO; import com.cloud.utils.Pair; +import com.cloud.utils.db.EntityManager; import com.cloud.utils.exception.CloudRuntimeException; +import org.apache.cloudstack.api.InternalIdentity; import org.apache.cloudstack.api.ServerApiException; import org.apache.cloudstack.api.command.QuotaConfigureEmailCmd; import org.apache.cloudstack.api.command.QuotaCreditsListCmd; @@ -186,6 +188,9 @@ public class QuotaResponseBuilderImplTest extends TestCase { @Mock User callerUserMock; + @Mock + EntityManager entityManagerMock; + @Before public void setup() { CallContext.register(callerUserMock, callerAccountMock); @@ -1135,4 +1140,35 @@ public void getDomainIdsForQuotaStatementTestThrowsAccessDeniedForProvidedDomain () -> quotaResponseBuilderSpy.getDomainIdsForQuotaStatement(null, 5L, false)); Mockito.verify(accountManagerMock).checkAccess(callerAccountMock, domainVoMock); } + + @Test(expected = InvalidParameterValueException.class) + public void retrieveResourceTestThrowsExceptionForInvalidUsageType() { + Integer invalidUsageType = 999; + quotaResponseBuilderSpy.retrieveResource("validUuid", invalidUsageType); + } + + @Test + public void retrieveResourceTestReturnsNullForNonexistentResource() { + String invalidUuid = "nonexistentUuid"; + Integer validUsageType = QuotaTypes.ALLOCATED_VM; + + Mockito.doReturn(null).when(entityManagerMock).findByUuidIncludingRemoved(Mockito.any(), Mockito.eq(invalidUuid)); + InternalIdentity result = quotaResponseBuilderSpy.retrieveResource(invalidUuid, validUsageType); + + Assert.assertNull(result); + } + + @Test + public void retrieveResourceTestReturnsCorrectResource() { + String validUuid = "validUuid"; + Integer validUsageType = QuotaTypes.ALLOCATED_VM; + InternalIdentity mockResource = Mockito.mock(InternalIdentity.class); + + Mockito.doReturn(mockResource).when(entityManagerMock).findByUuidIncludingRemoved(Mockito.any(), Mockito.eq(validUuid)); + + InternalIdentity result = quotaResponseBuilderSpy.retrieveResource(validUuid, validUsageType); + + Assert.assertNotNull(result); + Assert.assertEquals(mockResource, result); + } }