Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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 @@ -18,3 +18,12 @@
--;
-- Schema upgrade from 4.19.0.0 to 4.20.0.0
--;

-- 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_te1mplate_id` FOREIGN KEY (`email_template_id`) REFERENCES `cloud_usage`.`quota_email_templates`(`id`));
Comment thread
JoaoJandre marked this conversation as resolved.
Outdated
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,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 @@ -81,7 +83,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 @@ -143,52 +148,68 @@ public boolean stop() {
@Override
public void checkAndSendQuotaAlertEmails() {
List<DeferredQuotaEmail> deferredQuotaEmailList = new ArrayList<DeferredQuotaEmail>();
final BigDecimal zeroBalance = new BigDecimal(0);

s_logger.info("Checking and sending quota alert emails.");
for (final QuotaAccountVO quotaAccount : _quotaAcc.listAllQuotaAccount()) {
if (s_logger.isDebugEnabled()) {
s_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
}
if (s_logger.isDebugEnabled()) {
s_logger.debug("checkAndSendQuotaAlertEmails: Check id=" + account.getId() + " bal=" + accountBalance + ", alertDate=" + alertDate + ", lockable=" + lockable);
}
if (accountBalance.compareTo(zeroBalance) < 0) {
if (_lockAccountEnforcement && (lockable == 1)) {
if (_quotaManager.isLockable(account)) {
s_logger.info("Locking account " + account.getAccountName() + " due to quota < 0.");
lockAccount(account.getId());
}
}
if (alertDate == null || (balanceDate.after(alertDate) && getDifferenceDays(alertDate, new Date()) > 1)) {
s_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)) {
s_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 (s_logger.isDebugEnabled()) {
s_logger.debug("checkAndSendQuotaAlertEmails: Attempting to send quota alert email to users of account: " + emailToBeSent.getAccount().getAccountName());
}
s_logger.debug(String.format("Attempting to send a quota alert email to users of account [%s].", 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.
s_logger.debug(String.format("Checking %s for email alerts.", quotaAccount));
BigDecimal accountBalance = quotaAccount.getQuotaBalance();

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

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

Date balanceDate = quotaAccount.getQuotaBalanceDate();
Date alertDate = quotaAccount.getQuotaAlertDate();
int lockable = quotaAccount.getQuotaEnforce();
BigDecimal thresholdBalance = quotaAccount.getQuotaMinBalance();

s_logger.debug(String.format("Checking %s with accountBalance [%s], alertDate [%s] and lockable [%s] to see if a quota alert email should be sent.", account,
accountBalance, alertDate, lockable));

QuotaEmailConfigurationVO quotaEmpty = quotaEmailConfigurationDao.findByAccountIdAndEmailTemplateType(account.getAccountId(), QuotaEmailTemplateTypes.QUOTA_EMPTY);
QuotaEmailConfigurationVO quotaLow = quotaEmailConfigurationDao.findByAccountIdAndEmailTemplateType(account.getAccountId(), QuotaEmailTemplateTypes.QUOTA_LOW);

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)) {
s_logger.info(String.format("Locking %s, as quota balance is lower than 0.", account));
lockAccount(account.getId());
}
if (quotaEmpty != null && quotaEmpty.isEnabled() && shouldSendEmail) {
s_logger.debug(String.format("Adding %s 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 && quotaLow != null && quotaLow.isEnabled() && shouldSendEmail) {
s_logger.debug(String.format("Adding %s to the deferred emails list, as quota balance [%s] is below the threshold [%s].", account, accountBalance, thresholdBalance));
deferredQuotaEmailList.add(new DeferredQuotaEmail(account, quotaAccount, QuotaEmailTemplateTypes.QUOTA_LOW));
return;
}
s_logger.debug(String.format("%s 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 @@ -286,7 +307,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,8 +31,12 @@
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.apache.cloudstack.quota.vo.QuotaEmailConfigurationVO;
import org.apache.cloudstack.quota.vo.QuotaEmailTemplatesVO;
import org.apache.log4j.Logger;
import org.springframework.stereotype.Component;

Expand All @@ -55,6 +59,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 @@ -109,10 +119,16 @@ public boolean stop() {
public void sendStatement() {

List<DeferredQuotaEmail> deferredQuotaEmailList = new ArrayList<DeferredQuotaEmail>();
QuotaEmailTemplatesVO templateVO = quotaEmailTemplatesDao.listAllQuotaEmailTemplates(QuotaConfig.QuotaEmailTemplateTypes.QUOTA_STATEMENT.toString()).get(0);
for (final QuotaAccountVO quotaAccount : _quotaAcc.listAllQuotaAccount()) {
if (quotaAccount.getQuotaBalance() == null) {
continue; // no quota usage for this account ever, ignore
}
QuotaEmailConfigurationVO quotaEmailConfigurationVO = quotaEmailConfigurationDao.findByAccountIdAndEmailTemplateId(quotaAccount.getAccountId(), templateVO.getId());
if (quotaEmailConfigurationVO != null && !quotaEmailConfigurationVO.isEnabled()) {
s_logger.debug(String.format("%s has [%s] 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);
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);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// 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.GenericDaoBase;
import com.cloud.utils.db.JoinBuilder;
import com.cloud.utils.db.SearchBuilder;
import com.cloud.utils.db.SearchCriteria;
import com.cloud.utils.db.Transaction;
import com.cloud.utils.db.TransactionCallback;
import com.cloud.utils.db.TransactionCallbackNoReturn;
import com.cloud.utils.db.TransactionLegacy;
import com.cloud.utils.db.TransactionStatus;
import org.apache.cloudstack.quota.constant.QuotaConfig;
import org.apache.cloudstack.quota.vo.QuotaEmailConfigurationVO;
import org.apache.cloudstack.quota.vo.QuotaEmailTemplatesVO;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.inject.Inject;
import java.util.List;

@Component
public class QuotaEmailConfigurationDaoImpl extends GenericDaoBase<QuotaEmailConfigurationVO, Long> implements QuotaEmailConfigurationDao {

@Inject
private QuotaEmailTemplatesDao quotaEmailTemplatesDao;

private SearchBuilder<QuotaEmailConfigurationVO> searchBuilderFindByIds;

private SearchBuilder<QuotaEmailTemplatesVO> searchBuilderFindByTemplateName;

private SearchBuilder<QuotaEmailConfigurationVO> searchBuilderFindByTemplateTypeAndAccountId;

@PostConstruct
public void init() {
searchBuilderFindByIds = createSearchBuilder();
searchBuilderFindByIds.and("account_id", searchBuilderFindByIds.entity().getAccountId(), SearchCriteria.Op.EQ);
searchBuilderFindByIds.and("email_template_id", searchBuilderFindByIds.entity().getEmailTemplateId(), SearchCriteria.Op.EQ);
searchBuilderFindByIds.done();

searchBuilderFindByTemplateName = quotaEmailTemplatesDao.createSearchBuilder();
searchBuilderFindByTemplateName.and("template_name", searchBuilderFindByTemplateName.entity().getTemplateName(), SearchCriteria.Op.EQ);

searchBuilderFindByTemplateTypeAndAccountId = createSearchBuilder();
searchBuilderFindByTemplateTypeAndAccountId.and("account_id", searchBuilderFindByTemplateTypeAndAccountId.entity().getAccountId(), SearchCriteria.Op.EQ);
searchBuilderFindByTemplateTypeAndAccountId.join("email_template_id", searchBuilderFindByTemplateName, searchBuilderFindByTemplateName.entity().getId(),
searchBuilderFindByTemplateTypeAndAccountId.entity().getEmailTemplateId(), JoinBuilder.JoinType.INNER);

searchBuilderFindByTemplateName.done();
searchBuilderFindByTemplateTypeAndAccountId.done();
Comment thread
JoaoJandre marked this conversation as resolved.
}

@Override
public QuotaEmailConfigurationVO findByAccountIdAndEmailTemplateId(long accountId, long emailTemplateId) {
SearchCriteria<QuotaEmailConfigurationVO> sc = searchBuilderFindByIds.create();
sc.setParameters("account_id", accountId);
sc.setParameters("email_template_id", emailTemplateId);
return Transaction.execute(TransactionLegacy.USAGE_DB, new TransactionCallback<QuotaEmailConfigurationVO>() {
@Override
public QuotaEmailConfigurationVO doInTransaction(TransactionStatus status) {
return findOneBy(sc);
}
});
Comment thread
JoaoJandre marked this conversation as resolved.
Outdated
}

@Override
public QuotaEmailConfigurationVO updateQuotaEmailConfiguration(QuotaEmailConfigurationVO quotaEmailConfigurationVO) {
SearchCriteria<QuotaEmailConfigurationVO> sc = searchBuilderFindByIds.create();
sc.setParameters("account_id", quotaEmailConfigurationVO.getAccountId());
sc.setParameters("email_template_id", quotaEmailConfigurationVO.getEmailTemplateId());
Transaction.execute(TransactionLegacy.USAGE_DB, new TransactionCallbackNoReturn() {
@Override
public void doInTransactionWithoutResult(TransactionStatus status) {
update(quotaEmailConfigurationVO, sc);
}
});
Comment thread
JoaoJandre marked this conversation as resolved.
Outdated

return quotaEmailConfigurationVO;
}

@Override
public void persistQuotaEmailConfiguration(QuotaEmailConfigurationVO quotaEmailConfigurationVO) {
Transaction.execute(TransactionLegacy.USAGE_DB, new TransactionCallbackNoReturn() {
@Override
public void doInTransactionWithoutResult(TransactionStatus status) {
persist(quotaEmailConfigurationVO);
}
});
Comment thread
JoaoJandre marked this conversation as resolved.
Outdated
}

@Override
public List<QuotaEmailConfigurationVO> listByAccount(long accountId) {
SearchCriteria<QuotaEmailConfigurationVO> sc = searchBuilderFindByIds.create();
sc.setParameters("account_id", accountId);

return Transaction.execute(TransactionLegacy.USAGE_DB, new TransactionCallback<List<QuotaEmailConfigurationVO>>() {
@Override
public List<QuotaEmailConfigurationVO> doInTransaction(TransactionStatus status) {
return listBy(sc);
}
});
Comment thread
JoaoJandre marked this conversation as resolved.
Outdated
}

@Override
public QuotaEmailConfigurationVO findByAccountIdAndEmailTemplateType(long accountId, QuotaConfig.QuotaEmailTemplateTypes quotaEmailTemplateType) {
SearchCriteria<QuotaEmailConfigurationVO> sc = searchBuilderFindByTemplateTypeAndAccountId.create();
sc.setParameters("account_id", accountId);
sc.setJoinParameters("email_template_id", "template_name", quotaEmailTemplateType.toString());

return Transaction.execute(TransactionLegacy.USAGE_DB, new TransactionCallback<QuotaEmailConfigurationVO>() {
@Override
public QuotaEmailConfigurationVO doInTransaction(TransactionStatus status) {
return findOneBy(sc);
}
});
Comment thread
JoaoJandre marked this conversation as resolved.
Outdated
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,6 @@
public interface QuotaEmailTemplatesDao extends GenericDao<QuotaEmailTemplatesVO, Long> {
List<QuotaEmailTemplatesVO> listAllQuotaEmailTemplates(String templateName);
boolean updateQuotaEmailTemplate(QuotaEmailTemplatesVO template);

QuotaEmailTemplatesVO findById(long id);
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,14 @@ public Boolean doInTransaction(final TransactionStatus status) {
}
});
}

@Override
public QuotaEmailTemplatesVO findById(long id) {
return Transaction.execute(TransactionLegacy.USAGE_DB, new TransactionCallback<QuotaEmailTemplatesVO>() {
@Override
public QuotaEmailTemplatesVO doInTransaction(final TransactionStatus status) {
return QuotaEmailTemplatesDaoImpl.super.findById(id);
}
});
Comment thread
JoaoJandre marked this conversation as resolved.
Outdated
}
}
Loading