Skip to content
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
25b8808
Quota email configuration feature
JoaoJandre Sep 29, 2023
65683cf
Merge remote-tracking branch 'origin/main' into quota-email-configura…
JoaoJandre Nov 30, 2023
07a28d3
Merge remote-tracking branch 'origin/main' into quota-email-configura…
JoaoJandre Dec 1, 2023
7d5211a
fix double injecting
JoaoJandre Dec 5, 2023
b9efd14
Fix command versions
JoaoJandre Dec 5, 2023
eab165f
Merge remote-tracking branch 'origin/main' into quota-email-configura…
JoaoJandre Dec 19, 2023
7403bf6
Merge remote-tracking branch 'origin/main' into quota-email-configura…
JoaoJandre Feb 5, 2024
d9126ef
move sql to 4.20
JoaoJandre Feb 5, 2024
14b622a
Merge remote-tracking branch 'origin/main' into quota-email-configura…
JoaoJandre Feb 8, 2024
6d44213
Address reviews
JoaoJandre Feb 13, 2024
8a48766
Merge remote-tracking branch 'origin/main' into quota-email-configura…
JoaoJandre Feb 13, 2024
de99c9e
remove unused imports
JoaoJandre Feb 13, 2024
4129499
Address reviews and add config
JoaoJandre Feb 16, 2024
a64e5f9
Merge remote-tracking branch 'origin/main' into quota-email-configura…
JoaoJandre Feb 20, 2024
ff0c418
Merge remote-tracking branch 'origin/main' into quota-email-configura…
JoaoJandre Feb 27, 2024
efad281
Adress review
JoaoJandre Mar 1, 2024
aeeab86
Use lambda
JoaoJandre Mar 1, 2024
8cc13bc
Address reviews
JoaoJandre Mar 4, 2024
df88673
Merge remote-tracking branch 'origin/main' into quota-email-configura…
JoaoJandre Mar 4, 2024
fa027f6
fix log
JoaoJandre Mar 4, 2024
cc282dc
Merge remote-tracking branch 'origin/main' into quota-email-configura…
JoaoJandre Mar 8, 2024
9200140
Merge remote-tracking branch 'origin/main' into quota-email-configura…
JoaoJandre Mar 14, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,12 @@ UPDATE `cloud`.`service_offering` SET ram_size = 512 WHERE unique_name IN ("Clou
"Cloud.Com-InternalLBVm", "Cloud.Com-InternalLBVm-Local",
"Cloud.Com-ElasticLBVm", "Cloud.Com-ElasticLBVm-Local")
AND system_use = 1 AND ram_size < 512;

-- Create table to persist quota email template configurations
CREATE TABLE IF NOT EXISTS `cloud_usage`.`quota_email_configuration`(
`account_id` int(11) NOT NULL,
`email_template_id` bigint(20) NOT NULL,
`enabled` int(1) UNSIGNED NOT NULL,
PRIMARY KEY (`account_id`, `email_template_id`),
CONSTRAINT `FK_quota_email_configuration_account_id` FOREIGN KEY (`account_id`) REFERENCES `cloud_usage`.`quota_account`(`account_id`),
CONSTRAINT `FK_quota_email_configuration_email_template_id` FOREIGN KEY (`email_template_id`) REFERENCES `cloud_usage`.`quota_email_templates`(`id`));
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,14 @@
//under the License.
package org.apache.cloudstack.quota;

import com.cloud.user.AccountVO;
import com.cloud.utils.component.Manager;

import org.apache.cloudstack.quota.QuotaAlertManagerImpl.DeferredQuotaEmail;
import org.apache.cloudstack.quota.constant.QuotaConfig;

public interface QuotaAlertManager extends Manager {
boolean isQuotaEmailTypeEnabledForAccount(AccountVO account, QuotaConfig.QuotaEmailTemplateTypes quotaEmailTemplateType);
void checkAndSendQuotaAlertEmails();
void sendQuotaAlert(DeferredQuotaEmail emailToBeSent);
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,10 @@
import org.apache.cloudstack.quota.constant.QuotaConfig;
import org.apache.cloudstack.quota.constant.QuotaConfig.QuotaEmailTemplateTypes;
import org.apache.cloudstack.quota.dao.QuotaAccountDao;
import org.apache.cloudstack.quota.dao.QuotaEmailConfigurationDao;
import org.apache.cloudstack.quota.dao.QuotaEmailTemplatesDao;
import org.apache.cloudstack.quota.vo.QuotaAccountVO;
import org.apache.cloudstack.quota.vo.QuotaEmailConfigurationVO;
import org.apache.cloudstack.quota.vo.QuotaEmailTemplatesVO;
import org.apache.commons.lang.StringEscapeUtils;
import org.apache.commons.lang.text.StrSubstitutor;
Expand Down Expand Up @@ -80,7 +82,10 @@ public class QuotaAlertManagerImpl extends ManagerBase implements QuotaAlertMana
@Inject
private QuotaManager _quotaManager;

private boolean _lockAccountEnforcement = false;
@Inject
private QuotaEmailConfigurationDao quotaEmailConfigurationDao;

protected boolean _lockAccountEnforcement = false;
private String senderAddress;
protected SMTPMailSender mailSender;

Expand Down Expand Up @@ -139,55 +144,100 @@ public boolean stop() {
return true;
}

/**
* Returns whether a Quota email type is enabled or not for the provided account.
*/
@Override
public boolean isQuotaEmailTypeEnabledForAccount(AccountVO account, QuotaEmailTemplateTypes quotaEmailTemplateType) {
boolean quotaEmailsEnabled = QuotaConfig.QuotaEnableEmails.valueIn(account.getAccountId());
if (!quotaEmailsEnabled) {
logger.debug("Configuration [{}] is disabled for account [{}]. Therefore, the account will not receive Quota email of type [{}].", QuotaConfig.QuotaEnableEmails.key(), account, quotaEmailTemplateType);
return false;
}

QuotaEmailConfigurationVO quotaEmail = quotaEmailConfigurationDao.findByAccountIdAndEmailTemplateType(account.getAccountId(), quotaEmailTemplateType);

boolean emailEnabled = quotaEmail == null || quotaEmail.isEnabled();
if (emailEnabled) {
logger.debug("Quota email [{}] is enabled for account [{}].", quotaEmailTemplateType, account);
} else {
logger.debug("Quota email [{}] has been manually disabled for account [{}] through the API quotaConfigureEmail.", quotaEmailTemplateType, account);
}
return emailEnabled;
}


@Override
public void checkAndSendQuotaAlertEmails() {
List<DeferredQuotaEmail> deferredQuotaEmailList = new ArrayList<DeferredQuotaEmail>();
final BigDecimal zeroBalance = new BigDecimal(0);

logger.info("Checking and sending quota alert emails.");
for (final QuotaAccountVO quotaAccount : _quotaAcc.listAllQuotaAccount()) {
if (logger.isDebugEnabled()) {
logger.debug("checkAndSendQuotaAlertEmails accId=" + quotaAccount.getId());
}
BigDecimal accountBalance = quotaAccount.getQuotaBalance();
Date balanceDate = quotaAccount.getQuotaBalanceDate();
Date alertDate = quotaAccount.getQuotaAlertDate();
int lockable = quotaAccount.getQuotaEnforce();
BigDecimal thresholdBalance = quotaAccount.getQuotaMinBalance();
if (accountBalance != null) {
AccountVO account = _accountDao.findById(quotaAccount.getId());
if (account == null) {
continue; // the account is removed
}
logger.debug("checkAndSendQuotaAlertEmails: Check id={} bal={}, alertDate={}, lockable={}", account.getId(),
accountBalance, DateUtil.displayDateInTimezone(QuotaManagerImpl.getUsageAggregationTimeZone(), alertDate),
lockable);
if (accountBalance.compareTo(zeroBalance) < 0) {
if (_lockAccountEnforcement && (lockable == 1)) {
if (_quotaManager.isLockable(account)) {
logger.info("Locking account " + account.getAccountName() + " due to quota < 0.");
lockAccount(account.getId());
}
}
if (alertDate == null || (balanceDate.after(alertDate) && getDifferenceDays(alertDate, new Date()) > 1)) {
logger.info("Sending alert " + account.getAccountName() + " due to quota < 0.");
deferredQuotaEmailList.add(new DeferredQuotaEmail(account, quotaAccount, QuotaConfig.QuotaEmailTemplateTypes.QUOTA_EMPTY));
}
} else if (accountBalance.compareTo(thresholdBalance) < 0) {
if (alertDate == null || (balanceDate.after(alertDate) && getDifferenceDays(alertDate, new Date()) > 1)) {
logger.info("Sending alert " + account.getAccountName() + " due to quota below threshold.");
deferredQuotaEmailList.add(new DeferredQuotaEmail(account, quotaAccount, QuotaConfig.QuotaEmailTemplateTypes.QUOTA_LOW));
}
}
}
checkQuotaAlertEmailForAccount(deferredQuotaEmailList, quotaAccount);
}

for (DeferredQuotaEmail emailToBeSent : deferredQuotaEmailList) {
if (logger.isDebugEnabled()) {
logger.debug("checkAndSendQuotaAlertEmails: Attempting to send quota alert email to users of account: " + emailToBeSent.getAccount().getAccountName());
}
logger.debug("Attempting to send a quota alert email to users of account [{}].", emailToBeSent.getAccount().getAccountName());
sendQuotaAlert(emailToBeSent);
}
}

/**
* Checks a given quota account to see if they should receive any emails. First by checking if it has any balance at all, if its account can be found, then checks
* if they should receive either QUOTA_EMPTY or QUOTA_LOW emails, taking into account if these email templates are disabled or not for that account.
* */
protected void checkQuotaAlertEmailForAccount(List<DeferredQuotaEmail> deferredQuotaEmailList, QuotaAccountVO quotaAccount) {
Comment thread
JoaoJandre marked this conversation as resolved.
logger.debug("Checking {} for email alerts.", quotaAccount);
BigDecimal accountBalance = quotaAccount.getQuotaBalance();

if (accountBalance == null) {
logger.debug("{} has a null balance, therefore it will not receive quota alert emails.", quotaAccount);
return;
}

AccountVO account = _accountDao.findById(quotaAccount.getId());
if (account == null) {
logger.debug("Account of {} is removed, thus it will not receive quota alert emails.", quotaAccount);
return;
}

checkBalanceAndAddToEmailList(deferredQuotaEmailList, quotaAccount, account, accountBalance);
}

private void checkBalanceAndAddToEmailList(List<DeferredQuotaEmail> deferredQuotaEmailList, QuotaAccountVO quotaAccount, AccountVO account, BigDecimal accountBalance) {
Date balanceDate = quotaAccount.getQuotaBalanceDate();
Date alertDate = quotaAccount.getQuotaAlertDate();
int lockable = quotaAccount.getQuotaEnforce();
BigDecimal thresholdBalance = quotaAccount.getQuotaMinBalance();

logger.debug("Checking {} with accountBalance [{}], alertDate [{}] and lockable [{}] to see if a quota alert email should be sent.", account,
accountBalance, DateUtil.displayDateInTimezone(QuotaManagerImpl.getUsageAggregationTimeZone(), alertDate), lockable);

boolean shouldSendEmail = alertDate == null || (balanceDate.after(alertDate) && getDifferenceDays(alertDate, new Date()) > 1);

if (accountBalance.compareTo(BigDecimal.ZERO) < 0) {
if (_lockAccountEnforcement && lockable == 1 && _quotaManager.isLockable(account)) {
logger.info("Locking {}, as quota balance is lower than 0.", account);
lockAccount(account.getId());
}

boolean quotaEmptyEmailEnabled = isQuotaEmailTypeEnabledForAccount(account, QuotaEmailTemplateTypes.QUOTA_EMPTY);
if (quotaEmptyEmailEnabled && shouldSendEmail) {
logger.debug("Adding {} to the deferred emails list, as quota balance is lower than 0.", account);
deferredQuotaEmailList.add(new DeferredQuotaEmail(account, quotaAccount, QuotaEmailTemplateTypes.QUOTA_EMPTY));
return;
}
} else if (accountBalance.compareTo(thresholdBalance) < 0) {
boolean quotaLowEmailEnabled = isQuotaEmailTypeEnabledForAccount(account, QuotaEmailTemplateTypes.QUOTA_LOW);
if (quotaLowEmailEnabled && shouldSendEmail) {
logger.debug("Adding {} to the deferred emails list, as quota balance [{}] is below the threshold [{}].", account, accountBalance, thresholdBalance);
deferredQuotaEmailList.add(new DeferredQuotaEmail(account, quotaAccount, QuotaEmailTemplateTypes.QUOTA_LOW));
return;
}
}
logger.debug("{} will not receive any quota alert emails in this round.", account);
}

@Override
public void sendQuotaAlert(DeferredQuotaEmail emailToBeSent) {
final AccountVO account = emailToBeSent.getAccount();
Expand Down Expand Up @@ -285,7 +335,7 @@ public Map<String, String> generateOptionMap(AccountVO accountVO, String userNam
return optionMap;
}

public static long getDifferenceDays(Date d1, Date d2) {
public long getDifferenceDays(Date d1, Date d2) {
long diff = d2.getTime() - d1.getTime();
return TimeUnit.DAYS.convert(diff, TimeUnit.MILLISECONDS);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
import org.apache.cloudstack.quota.QuotaAlertManagerImpl.DeferredQuotaEmail;
import org.apache.cloudstack.quota.constant.QuotaConfig;
import org.apache.cloudstack.quota.dao.QuotaAccountDao;
import org.apache.cloudstack.quota.dao.QuotaEmailConfigurationDao;
import org.apache.cloudstack.quota.dao.QuotaEmailTemplatesDao;
import org.apache.cloudstack.quota.dao.QuotaUsageDao;
import org.apache.cloudstack.quota.vo.QuotaAccountVO;
import org.springframework.stereotype.Component;
Expand All @@ -53,6 +55,12 @@ public class QuotaStatementImpl extends ManagerBase implements QuotaStatement {
@Inject
private ConfigurationDao _configDao;

@Inject
private QuotaEmailConfigurationDao quotaEmailConfigurationDao;

@Inject
private QuotaEmailTemplatesDao quotaEmailTemplatesDao;

final public static int s_LAST_STATEMENT_SENT_DAYS = 6; //ideally should be less than 7 days

public enum QuotaStatementPeriods {
Expand Down Expand Up @@ -111,29 +119,34 @@ public void sendStatement() {
if (quotaAccount.getQuotaBalance() == null) {
continue; // no quota usage for this account ever, ignore
}
AccountVO account = _accountDao.findById(quotaAccount.getId());
if (account == null) {
logger.debug("Could not find an account corresponding to [{}]. Therefore, the statement email will not be sent.", quotaAccount);
continue;
}

boolean quotaStatementEmailEnabled = _quotaAlert.isQuotaEmailTypeEnabledForAccount(account, QuotaConfig.QuotaEmailTemplateTypes.QUOTA_STATEMENT);
if (!quotaStatementEmailEnabled) {
logger.debug("{} has [{}] email disabled. Therefore the email will not be sent.", quotaAccount, QuotaConfig.QuotaEmailTemplateTypes.QUOTA_STATEMENT);
continue;
}

//check if it is statement time
Calendar interval[] = statementTime(Calendar.getInstance(), _period);

Date lastStatementDate = quotaAccount.getLastStatementDate();
if (interval != null) {
AccountVO account = _accountDao.findById(quotaAccount.getId());
if (account != null) {
if (lastStatementDate == null || getDifferenceDays(lastStatementDate, new Date()) >= s_LAST_STATEMENT_SENT_DAYS + 1) {
BigDecimal quotaUsage = _quotaUsage.findTotalQuotaUsage(account.getAccountId(), account.getDomainId(), null, interval[0].getTime(), interval[1].getTime());
logger.info("For account=" + quotaAccount.getId() + ", quota used = " + quotaUsage);
// send statement
deferredQuotaEmailList.add(new DeferredQuotaEmail(account, quotaAccount, quotaUsage, QuotaConfig.QuotaEmailTemplateTypes.QUOTA_STATEMENT));
} else {
if (logger.isDebugEnabled()) {
logger.debug("For " + quotaAccount.getId() + " the statement has been sent recently");

}
}
if (lastStatementDate == null || getDifferenceDays(lastStatementDate, new Date()) >= s_LAST_STATEMENT_SENT_DAYS + 1) {
BigDecimal quotaUsage = _quotaUsage.findTotalQuotaUsage(account.getAccountId(), account.getDomainId(), null, interval[0].getTime(), interval[1].getTime());
logger.info("Quota statement for account [{}] has an usage of [{}].", quotaAccount, quotaUsage);

// send statement
deferredQuotaEmailList.add(new DeferredQuotaEmail(account, quotaAccount, quotaUsage, QuotaConfig.QuotaEmailTemplateTypes.QUOTA_STATEMENT));
} else {
logger.debug("Quota statement has already been sent recently to account [{}].", quotaAccount);
}
} else if (lastStatementDate != null) {
logger.info("For " + quotaAccount.getId() + " it is already more than " + getDifferenceDays(lastStatementDate, new Date())
+ " days, will send statement in next cycle");
logger.info("For account {} it is already more than {} days, will send statement in next cycle.", quotaAccount.getId(), getDifferenceDays(lastStatementDate, new Date()));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ public interface QuotaConfig {
ConfigKey<String> QuotaEmailFooter = new ConfigKey<>("Advanced", String.class, "quota.email.footer", "",
"Text to be added as a footer for quota emails. Line breaks are not automatically inserted between this section and the body.", true, ConfigKey.Scope.Domain);

ConfigKey<Boolean> QuotaEnableEmails = new ConfigKey<>("Advanced", Boolean.class, "quota.enable.emails", "true",
"Indicates whether Quota emails should be sent or not to accounts. When enabled, the behavior for each account can be overridden through the API quotaConfigureEmail.", true, ConfigKey.Scope.Account);

enum QuotaEmailTemplateTypes {
QUOTA_LOW, QUOTA_EMPTY, QUOTA_UNLOCK_ACCOUNT, QUOTA_STATEMENT
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// 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.quota.dao;

import com.cloud.utils.db.GenericDao;
import org.apache.cloudstack.quota.constant.QuotaConfig;
import org.apache.cloudstack.quota.vo.QuotaEmailConfigurationVO;

import java.util.List;

public interface QuotaEmailConfigurationDao extends GenericDao<QuotaEmailConfigurationVO, Long> {

QuotaEmailConfigurationVO findByAccountIdAndEmailTemplateId(long accountId, long emailTemplateId);

QuotaEmailConfigurationVO updateQuotaEmailConfiguration(QuotaEmailConfigurationVO quotaEmailConfigurationVO);

void persistQuotaEmailConfiguration(QuotaEmailConfigurationVO quotaEmailConfigurationVO);

List<QuotaEmailConfigurationVO> listByAccount(long accountId);

QuotaEmailConfigurationVO findByAccountIdAndEmailTemplateType(long accountId, QuotaConfig.QuotaEmailTemplateTypes quotaEmailTemplateType);
}
Loading