Skip to content

Commit f76c6f3

Browse files
authored
Quota email configuration (apache#8307)
* Quota email configuration feature
1 parent 7b02c4c commit f76c6f3

20 files changed

+988
-91
lines changed

engine/schema/src/main/resources/META-INF/db/schema-41900to42000.sql

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,13 @@ CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.network_offerings','for_nsx', 'int(1
6969
CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.network_offerings','nsx_mode', 'varchar(32) COMMENT "mode in which the network would route traffic"');
7070
CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.vpc_offerings','for_nsx', 'int(1) unsigned DEFAULT "0" COMMENT "is nsx enabled for the resource"');
7171
CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.vpc_offerings','nsx_mode', 'varchar(32) COMMENT "mode in which the network would route traffic"');
72+
73+
74+
-- Create table to persist quota email template configurations
75+
CREATE TABLE IF NOT EXISTS `cloud_usage`.`quota_email_configuration`(
76+
`account_id` int(11) NOT NULL,
77+
`email_template_id` bigint(20) NOT NULL,
78+
`enabled` int(1) UNSIGNED NOT NULL,
79+
PRIMARY KEY (`account_id`, `email_template_id`),
80+
CONSTRAINT `FK_quota_email_configuration_account_id` FOREIGN KEY (`account_id`) REFERENCES `cloud_usage`.`quota_account`(`account_id`),
81+
CONSTRAINT `FK_quota_email_configuration_email_template_id` FOREIGN KEY (`email_template_id`) REFERENCES `cloud_usage`.`quota_email_templates`(`id`));

framework/quota/src/main/java/org/apache/cloudstack/quota/QuotaAlertManager.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,14 @@
1616
//under the License.
1717
package org.apache.cloudstack.quota;
1818

19+
import com.cloud.user.AccountVO;
1920
import com.cloud.utils.component.Manager;
2021

2122
import org.apache.cloudstack.quota.QuotaAlertManagerImpl.DeferredQuotaEmail;
23+
import org.apache.cloudstack.quota.constant.QuotaConfig;
2224

2325
public interface QuotaAlertManager extends Manager {
26+
boolean isQuotaEmailTypeEnabledForAccount(AccountVO account, QuotaConfig.QuotaEmailTemplateTypes quotaEmailTemplateType);
2427
void checkAndSendQuotaAlertEmails();
2528
void sendQuotaAlert(DeferredQuotaEmail emailToBeSent);
2629
}

framework/quota/src/main/java/org/apache/cloudstack/quota/QuotaAlertManagerImpl.java

Lines changed: 90 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,10 @@
3434
import org.apache.cloudstack.quota.constant.QuotaConfig;
3535
import org.apache.cloudstack.quota.constant.QuotaConfig.QuotaEmailTemplateTypes;
3636
import org.apache.cloudstack.quota.dao.QuotaAccountDao;
37+
import org.apache.cloudstack.quota.dao.QuotaEmailConfigurationDao;
3738
import org.apache.cloudstack.quota.dao.QuotaEmailTemplatesDao;
3839
import org.apache.cloudstack.quota.vo.QuotaAccountVO;
40+
import org.apache.cloudstack.quota.vo.QuotaEmailConfigurationVO;
3941
import org.apache.cloudstack.quota.vo.QuotaEmailTemplatesVO;
4042
import org.apache.commons.lang.StringEscapeUtils;
4143
import org.apache.commons.lang.text.StrSubstitutor;
@@ -80,7 +82,10 @@ public class QuotaAlertManagerImpl extends ManagerBase implements QuotaAlertMana
8082
@Inject
8183
private QuotaManager _quotaManager;
8284

83-
private boolean _lockAccountEnforcement = false;
85+
@Inject
86+
private QuotaEmailConfigurationDao quotaEmailConfigurationDao;
87+
88+
protected boolean _lockAccountEnforcement = false;
8489
private String senderAddress;
8590
protected SMTPMailSender mailSender;
8691

@@ -139,55 +144,100 @@ public boolean stop() {
139144
return true;
140145
}
141146

147+
/**
148+
* Returns whether a Quota email type is enabled or not for the provided account.
149+
*/
150+
@Override
151+
public boolean isQuotaEmailTypeEnabledForAccount(AccountVO account, QuotaEmailTemplateTypes quotaEmailTemplateType) {
152+
boolean quotaEmailsEnabled = QuotaConfig.QuotaEnableEmails.valueIn(account.getAccountId());
153+
if (!quotaEmailsEnabled) {
154+
logger.debug("Configuration [{}] is disabled for account [{}]. Therefore, the account will not receive Quota email of type [{}].", QuotaConfig.QuotaEnableEmails.key(), account, quotaEmailTemplateType);
155+
return false;
156+
}
157+
158+
QuotaEmailConfigurationVO quotaEmail = quotaEmailConfigurationDao.findByAccountIdAndEmailTemplateType(account.getAccountId(), quotaEmailTemplateType);
159+
160+
boolean emailEnabled = quotaEmail == null || quotaEmail.isEnabled();
161+
if (emailEnabled) {
162+
logger.debug("Quota email [{}] is enabled for account [{}].", quotaEmailTemplateType, account);
163+
} else {
164+
logger.debug("Quota email [{}] has been manually disabled for account [{}] through the API quotaConfigureEmail.", quotaEmailTemplateType, account);
165+
}
166+
return emailEnabled;
167+
}
168+
169+
142170
@Override
143171
public void checkAndSendQuotaAlertEmails() {
144172
List<DeferredQuotaEmail> deferredQuotaEmailList = new ArrayList<DeferredQuotaEmail>();
145-
final BigDecimal zeroBalance = new BigDecimal(0);
173+
174+
logger.info("Checking and sending quota alert emails.");
146175
for (final QuotaAccountVO quotaAccount : _quotaAcc.listAllQuotaAccount()) {
147-
if (logger.isDebugEnabled()) {
148-
logger.debug("checkAndSendQuotaAlertEmails accId=" + quotaAccount.getId());
149-
}
150-
BigDecimal accountBalance = quotaAccount.getQuotaBalance();
151-
Date balanceDate = quotaAccount.getQuotaBalanceDate();
152-
Date alertDate = quotaAccount.getQuotaAlertDate();
153-
int lockable = quotaAccount.getQuotaEnforce();
154-
BigDecimal thresholdBalance = quotaAccount.getQuotaMinBalance();
155-
if (accountBalance != null) {
156-
AccountVO account = _accountDao.findById(quotaAccount.getId());
157-
if (account == null) {
158-
continue; // the account is removed
159-
}
160-
logger.debug("checkAndSendQuotaAlertEmails: Check id={} bal={}, alertDate={}, lockable={}", account.getId(),
161-
accountBalance, DateUtil.displayDateInTimezone(QuotaManagerImpl.getUsageAggregationTimeZone(), alertDate),
162-
lockable);
163-
if (accountBalance.compareTo(zeroBalance) < 0) {
164-
if (_lockAccountEnforcement && (lockable == 1)) {
165-
if (_quotaManager.isLockable(account)) {
166-
logger.info("Locking account " + account.getAccountName() + " due to quota < 0.");
167-
lockAccount(account.getId());
168-
}
169-
}
170-
if (alertDate == null || (balanceDate.after(alertDate) && getDifferenceDays(alertDate, new Date()) > 1)) {
171-
logger.info("Sending alert " + account.getAccountName() + " due to quota < 0.");
172-
deferredQuotaEmailList.add(new DeferredQuotaEmail(account, quotaAccount, QuotaConfig.QuotaEmailTemplateTypes.QUOTA_EMPTY));
173-
}
174-
} else if (accountBalance.compareTo(thresholdBalance) < 0) {
175-
if (alertDate == null || (balanceDate.after(alertDate) && getDifferenceDays(alertDate, new Date()) > 1)) {
176-
logger.info("Sending alert " + account.getAccountName() + " due to quota below threshold.");
177-
deferredQuotaEmailList.add(new DeferredQuotaEmail(account, quotaAccount, QuotaConfig.QuotaEmailTemplateTypes.QUOTA_LOW));
178-
}
179-
}
180-
}
176+
checkQuotaAlertEmailForAccount(deferredQuotaEmailList, quotaAccount);
181177
}
182178

183179
for (DeferredQuotaEmail emailToBeSent : deferredQuotaEmailList) {
184-
if (logger.isDebugEnabled()) {
185-
logger.debug("checkAndSendQuotaAlertEmails: Attempting to send quota alert email to users of account: " + emailToBeSent.getAccount().getAccountName());
186-
}
180+
logger.debug("Attempting to send a quota alert email to users of account [{}].", emailToBeSent.getAccount().getAccountName());
187181
sendQuotaAlert(emailToBeSent);
188182
}
189183
}
190184

185+
/**
186+
* 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
187+
* 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.
188+
* */
189+
protected void checkQuotaAlertEmailForAccount(List<DeferredQuotaEmail> deferredQuotaEmailList, QuotaAccountVO quotaAccount) {
190+
logger.debug("Checking {} for email alerts.", quotaAccount);
191+
BigDecimal accountBalance = quotaAccount.getQuotaBalance();
192+
193+
if (accountBalance == null) {
194+
logger.debug("{} has a null balance, therefore it will not receive quota alert emails.", quotaAccount);
195+
return;
196+
}
197+
198+
AccountVO account = _accountDao.findById(quotaAccount.getId());
199+
if (account == null) {
200+
logger.debug("Account of {} is removed, thus it will not receive quota alert emails.", quotaAccount);
201+
return;
202+
}
203+
204+
checkBalanceAndAddToEmailList(deferredQuotaEmailList, quotaAccount, account, accountBalance);
205+
}
206+
207+
private void checkBalanceAndAddToEmailList(List<DeferredQuotaEmail> deferredQuotaEmailList, QuotaAccountVO quotaAccount, AccountVO account, BigDecimal accountBalance) {
208+
Date balanceDate = quotaAccount.getQuotaBalanceDate();
209+
Date alertDate = quotaAccount.getQuotaAlertDate();
210+
int lockable = quotaAccount.getQuotaEnforce();
211+
BigDecimal thresholdBalance = quotaAccount.getQuotaMinBalance();
212+
213+
logger.debug("Checking {} with accountBalance [{}], alertDate [{}] and lockable [{}] to see if a quota alert email should be sent.", account,
214+
accountBalance, DateUtil.displayDateInTimezone(QuotaManagerImpl.getUsageAggregationTimeZone(), alertDate), lockable);
215+
216+
boolean shouldSendEmail = alertDate == null || (balanceDate.after(alertDate) && getDifferenceDays(alertDate, new Date()) > 1);
217+
218+
if (accountBalance.compareTo(BigDecimal.ZERO) < 0) {
219+
if (_lockAccountEnforcement && lockable == 1 && _quotaManager.isLockable(account)) {
220+
logger.info("Locking {}, as quota balance is lower than 0.", account);
221+
lockAccount(account.getId());
222+
}
223+
224+
boolean quotaEmptyEmailEnabled = isQuotaEmailTypeEnabledForAccount(account, QuotaEmailTemplateTypes.QUOTA_EMPTY);
225+
if (quotaEmptyEmailEnabled && shouldSendEmail) {
226+
logger.debug("Adding {} to the deferred emails list, as quota balance is lower than 0.", account);
227+
deferredQuotaEmailList.add(new DeferredQuotaEmail(account, quotaAccount, QuotaEmailTemplateTypes.QUOTA_EMPTY));
228+
return;
229+
}
230+
} else if (accountBalance.compareTo(thresholdBalance) < 0) {
231+
boolean quotaLowEmailEnabled = isQuotaEmailTypeEnabledForAccount(account, QuotaEmailTemplateTypes.QUOTA_LOW);
232+
if (quotaLowEmailEnabled && shouldSendEmail) {
233+
logger.debug("Adding {} to the deferred emails list, as quota balance [{}] is below the threshold [{}].", account, accountBalance, thresholdBalance);
234+
deferredQuotaEmailList.add(new DeferredQuotaEmail(account, quotaAccount, QuotaEmailTemplateTypes.QUOTA_LOW));
235+
return;
236+
}
237+
}
238+
logger.debug("{} will not receive any quota alert emails in this round.", account);
239+
}
240+
191241
@Override
192242
public void sendQuotaAlert(DeferredQuotaEmail emailToBeSent) {
193243
final AccountVO account = emailToBeSent.getAccount();
@@ -285,7 +335,7 @@ public Map<String, String> generateOptionMap(AccountVO accountVO, String userNam
285335
return optionMap;
286336
}
287337

288-
public static long getDifferenceDays(Date d1, Date d2) {
338+
public long getDifferenceDays(Date d1, Date d2) {
289339
long diff = d2.getTime() - d1.getTime();
290340
return TimeUnit.DAYS.convert(diff, TimeUnit.MILLISECONDS);
291341
}

framework/quota/src/main/java/org/apache/cloudstack/quota/QuotaStatementImpl.java

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
import org.apache.cloudstack.quota.QuotaAlertManagerImpl.DeferredQuotaEmail;
3232
import org.apache.cloudstack.quota.constant.QuotaConfig;
3333
import org.apache.cloudstack.quota.dao.QuotaAccountDao;
34+
import org.apache.cloudstack.quota.dao.QuotaEmailConfigurationDao;
35+
import org.apache.cloudstack.quota.dao.QuotaEmailTemplatesDao;
3436
import org.apache.cloudstack.quota.dao.QuotaUsageDao;
3537
import org.apache.cloudstack.quota.vo.QuotaAccountVO;
3638
import org.springframework.stereotype.Component;
@@ -53,6 +55,12 @@ public class QuotaStatementImpl extends ManagerBase implements QuotaStatement {
5355
@Inject
5456
private ConfigurationDao _configDao;
5557

58+
@Inject
59+
private QuotaEmailConfigurationDao quotaEmailConfigurationDao;
60+
61+
@Inject
62+
private QuotaEmailTemplatesDao quotaEmailTemplatesDao;
63+
5664
final public static int s_LAST_STATEMENT_SENT_DAYS = 6; //ideally should be less than 7 days
5765

5866
public enum QuotaStatementPeriods {
@@ -111,29 +119,34 @@ public void sendStatement() {
111119
if (quotaAccount.getQuotaBalance() == null) {
112120
continue; // no quota usage for this account ever, ignore
113121
}
122+
AccountVO account = _accountDao.findById(quotaAccount.getId());
123+
if (account == null) {
124+
logger.debug("Could not find an account corresponding to [{}]. Therefore, the statement email will not be sent.", quotaAccount);
125+
continue;
126+
}
127+
128+
boolean quotaStatementEmailEnabled = _quotaAlert.isQuotaEmailTypeEnabledForAccount(account, QuotaConfig.QuotaEmailTemplateTypes.QUOTA_STATEMENT);
129+
if (!quotaStatementEmailEnabled) {
130+
logger.debug("{} has [{}] email disabled. Therefore the email will not be sent.", quotaAccount, QuotaConfig.QuotaEmailTemplateTypes.QUOTA_STATEMENT);
131+
continue;
132+
}
114133

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

118137
Date lastStatementDate = quotaAccount.getLastStatementDate();
119138
if (interval != null) {
120-
AccountVO account = _accountDao.findById(quotaAccount.getId());
121-
if (account != null) {
122-
if (lastStatementDate == null || getDifferenceDays(lastStatementDate, new Date()) >= s_LAST_STATEMENT_SENT_DAYS + 1) {
123-
BigDecimal quotaUsage = _quotaUsage.findTotalQuotaUsage(account.getAccountId(), account.getDomainId(), null, interval[0].getTime(), interval[1].getTime());
124-
logger.info("For account=" + quotaAccount.getId() + ", quota used = " + quotaUsage);
125-
// send statement
126-
deferredQuotaEmailList.add(new DeferredQuotaEmail(account, quotaAccount, quotaUsage, QuotaConfig.QuotaEmailTemplateTypes.QUOTA_STATEMENT));
127-
} else {
128-
if (logger.isDebugEnabled()) {
129-
logger.debug("For " + quotaAccount.getId() + " the statement has been sent recently");
130-
131-
}
132-
}
139+
if (lastStatementDate == null || getDifferenceDays(lastStatementDate, new Date()) >= s_LAST_STATEMENT_SENT_DAYS + 1) {
140+
BigDecimal quotaUsage = _quotaUsage.findTotalQuotaUsage(account.getAccountId(), account.getDomainId(), null, interval[0].getTime(), interval[1].getTime());
141+
logger.info("Quota statement for account [{}] has an usage of [{}].", quotaAccount, quotaUsage);
142+
143+
// send statement
144+
deferredQuotaEmailList.add(new DeferredQuotaEmail(account, quotaAccount, quotaUsage, QuotaConfig.QuotaEmailTemplateTypes.QUOTA_STATEMENT));
145+
} else {
146+
logger.debug("Quota statement has already been sent recently to account [{}].", quotaAccount);
133147
}
134148
} else if (lastStatementDate != null) {
135-
logger.info("For " + quotaAccount.getId() + " it is already more than " + getDifferenceDays(lastStatementDate, new Date())
136-
+ " days, will send statement in next cycle");
149+
logger.info("For account {} it is already more than {} days, will send statement in next cycle.", quotaAccount.getId(), getDifferenceDays(lastStatementDate, new Date()));
137150
}
138151
}
139152

framework/quota/src/main/java/org/apache/cloudstack/quota/constant/QuotaConfig.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,9 @@ public interface QuotaConfig {
7272
ConfigKey<String> QuotaEmailFooter = new ConfigKey<>("Advanced", String.class, "quota.email.footer", "",
7373
"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);
7474

75+
ConfigKey<Boolean> QuotaEnableEmails = new ConfigKey<>("Advanced", Boolean.class, "quota.enable.emails", "true",
76+
"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);
77+
7578
enum QuotaEmailTemplateTypes {
7679
QUOTA_LOW, QUOTA_EMPTY, QUOTA_UNLOCK_ACCOUNT, QUOTA_STATEMENT
7780
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
package org.apache.cloudstack.quota.dao;
18+
19+
import com.cloud.utils.db.GenericDao;
20+
import org.apache.cloudstack.quota.constant.QuotaConfig;
21+
import org.apache.cloudstack.quota.vo.QuotaEmailConfigurationVO;
22+
23+
import java.util.List;
24+
25+
public interface QuotaEmailConfigurationDao extends GenericDao<QuotaEmailConfigurationVO, Long> {
26+
27+
QuotaEmailConfigurationVO findByAccountIdAndEmailTemplateId(long accountId, long emailTemplateId);
28+
29+
QuotaEmailConfigurationVO updateQuotaEmailConfiguration(QuotaEmailConfigurationVO quotaEmailConfigurationVO);
30+
31+
void persistQuotaEmailConfiguration(QuotaEmailConfigurationVO quotaEmailConfigurationVO);
32+
33+
List<QuotaEmailConfigurationVO> listByAccount(long accountId);
34+
35+
QuotaEmailConfigurationVO findByAccountIdAndEmailTemplateType(long accountId, QuotaConfig.QuotaEmailTemplateTypes quotaEmailTemplateType);
36+
}

0 commit comments

Comments
 (0)