blob: 8ef2c96f2aabbd820f4ac3d978d3102edae103c8 [file] [log] [blame]
//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;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import javax.naming.ConfigurationException;
import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
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.QuotaEmailTemplatesDao;
import org.apache.cloudstack.quota.vo.QuotaAccountVO;
import org.apache.cloudstack.quota.vo.QuotaEmailTemplatesVO;
import org.apache.commons.lang.StringEscapeUtils;
import org.apache.commons.lang.text.StrSubstitutor;
import org.apache.log4j.Logger;
import org.springframework.stereotype.Component;
import com.cloud.domain.DomainVO;
import com.cloud.domain.dao.DomainDao;
import com.cloud.user.Account;
import com.cloud.user.Account.State;
import com.cloud.user.AccountVO;
import com.cloud.user.UserVO;
import com.cloud.user.dao.AccountDao;
import com.cloud.user.dao.UserDao;
import com.cloud.utils.component.ManagerBase;
import com.cloud.utils.db.TransactionLegacy;
import java.util.HashSet;
import java.util.Set;
import org.apache.cloudstack.utils.mailing.MailAddress;
import org.apache.cloudstack.utils.mailing.SMTPMailProperties;
import org.apache.cloudstack.utils.mailing.SMTPMailSender;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.BooleanUtils;
@Component
public class QuotaAlertManagerImpl extends ManagerBase implements QuotaAlertManager {
private static final Logger s_logger = Logger.getLogger(QuotaAlertManagerImpl.class);
@Inject
private AccountDao _accountDao;
@Inject
private QuotaAccountDao _quotaAcc;
@Inject
private UserDao _userDao;
@Inject
private DomainDao _domainDao;
@Inject
private QuotaEmailTemplatesDao _quotaEmailTemplateDao;
@Inject
private ConfigurationDao _configDao;
@Inject
private QuotaManager _quotaManager;
private boolean _lockAccountEnforcement = false;
private String senderAddress;
protected SMTPMailSender mailSender;
boolean _smtpDebug = false;
static final String ACCOUNT_NAME = "accountName";
static final String ACCOUNT_USERS = "accountUsers";
static final String DOMAIN_NAME = "domainName";
public QuotaAlertManagerImpl() {
super();
}
private void mergeConfigs(Map<String, String> dbParams, Map<String, Object> xmlParams) {
for (Map.Entry<String, Object> param : xmlParams.entrySet()) {
dbParams.put(param.getKey(), (String)param.getValue());
}
}
@Override
public boolean configure(String name, Map<String, Object> params) throws ConfigurationException {
super.configure(name, params);
Map<String, String> configs = _configDao.getConfiguration(params);
if (params != null) {
mergeConfigs(configs, params);
}
senderAddress = configs.get(QuotaConfig.QuotaSmtpSender.key());
_lockAccountEnforcement = BooleanUtils.toBoolean(configs.get(QuotaConfig.QuotaEnableEnforcement.key()));
String smtpUsername = configs.get(QuotaConfig.QuotaSmtpUser.key());
String namespace = "quota.usage.smtp";
configs.put(String.format("%s.debug", namespace), String.valueOf(_smtpDebug));
configs.put(String.format("%s.username", namespace), smtpUsername);
mailSender = new SMTPMailSender(configs, namespace);
return true;
}
@Override
public boolean start() {
if (s_logger.isInfoEnabled()) {
s_logger.info("Starting Alert Manager");
}
return true;
}
@Override
public boolean stop() {
if (s_logger.isInfoEnabled()) {
s_logger.info("Stopping Alert Manager");
}
return true;
}
@Override
public void checkAndSendQuotaAlertEmails() {
List<DeferredQuotaEmail> deferredQuotaEmailList = new ArrayList<DeferredQuotaEmail>();
final BigDecimal zeroBalance = new BigDecimal(0);
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));
}
}
}
}
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());
}
sendQuotaAlert(emailToBeSent);
}
}
@Override
public void sendQuotaAlert(DeferredQuotaEmail emailToBeSent) {
final AccountVO account = emailToBeSent.getAccount();
final BigDecimal balance = emailToBeSent.getQuotaBalance();
final BigDecimal usage = emailToBeSent.getQuotaUsage();
final QuotaConfig.QuotaEmailTemplateTypes emailType = emailToBeSent.getEmailTemplateType();
final List<QuotaEmailTemplatesVO> emailTemplates = _quotaEmailTemplateDao.listAllQuotaEmailTemplates(emailType.toString());
if (emailTemplates != null && emailTemplates.get(0) != null) {
final QuotaEmailTemplatesVO emailTemplate = emailTemplates.get(0);
final DomainVO accountDomain = _domainDao.findByIdIncludingRemoved(account.getDomainId());
final List<UserVO> usersInAccount = _userDao.listByAccount(account.getId());
String userNames = "";
final List<String> emailRecipients = new ArrayList<String>();
for (UserVO user : usersInAccount) {
userNames += String.format("%s <%s>,", user.getUsername(), user.getEmail());
emailRecipients.add(user.getEmail());
}
if (userNames.endsWith(",")) {
userNames = userNames.substring(0, userNames.length() - 1);
}
final Map<String, String> subjectOptionMap = generateOptionMap(account, userNames, accountDomain, balance, usage, emailType, false);
final Map<String, String> bodyOptionMap = generateOptionMap(account, userNames, accountDomain, balance, usage, emailType, true);
if (s_logger.isDebugEnabled()) {
s_logger.debug(String.format("Sending quota alert with values: accountName [%s], accountID [%s], accountUsers [%s], domainName [%s], domainID [%s].",
account.getAccountName(), account.getUuid(), userNames, accountDomain.getName(), accountDomain.getUuid()));
}
final StrSubstitutor subjectSubstitutor = new StrSubstitutor(subjectOptionMap);
final String subject = subjectSubstitutor.replace(emailTemplate.getTemplateSubject());
final StrSubstitutor bodySubstitutor = new StrSubstitutor(bodyOptionMap);
final String body = bodySubstitutor.replace(emailTemplate.getTemplateBody());
try {
sendQuotaAlert(account.getUuid(), emailRecipients, subject, body);
emailToBeSent.sentSuccessfully(_quotaAcc);
} catch (Exception e) {
s_logger.error(String.format("Unable to send quota alert email (subject=%s; body=%s) to account %s (%s) recipients (%s) due to error (%s)", subject, body, account.getAccountName(),
account.getUuid(), emailRecipients, e));
if (s_logger.isDebugEnabled()) {
s_logger.debug("Exception", e);
}
}
} else {
s_logger.error(String.format("No quota email template found for type %s, cannot send quota alert email to account %s(%s)", emailType, account.getAccountName(), account.getUuid()));
}
}
/*
*
*
*/
public Map<String, String> generateOptionMap(AccountVO accountVO, String userNames, DomainVO domainVO, final BigDecimal balance, final BigDecimal usage,
final QuotaConfig.QuotaEmailTemplateTypes emailType, boolean escapeHtml) {
final Map<String, String> optionMap = new HashMap<>();
optionMap.put("accountID", accountVO.getUuid());
optionMap.put("domainID", domainVO.getUuid());
optionMap.put("quotaBalance", QuotaConfig.QuotaCurrencySymbol.value() + " " + balance.toString());
if (emailType == QuotaEmailTemplateTypes.QUOTA_STATEMENT) {
optionMap.put("quotaUsage", QuotaConfig.QuotaCurrencySymbol.value() + " " + usage.toString());
}
if (escapeHtml) {
optionMap.put(ACCOUNT_NAME, StringEscapeUtils.escapeHtml(accountVO.getAccountName()));
optionMap.put(ACCOUNT_USERS, StringEscapeUtils.escapeHtml(userNames));
optionMap.put(DOMAIN_NAME, StringEscapeUtils.escapeHtml(domainVO.getName()));
return optionMap;
}
optionMap.put(ACCOUNT_NAME, accountVO.getAccountName());
optionMap.put(ACCOUNT_USERS, userNames);
optionMap.put(DOMAIN_NAME, domainVO.getName());
return optionMap;
}
public static long getDifferenceDays(Date d1, Date d2) {
long diff = d2.getTime() - d1.getTime();
return TimeUnit.DAYS.convert(diff, TimeUnit.MILLISECONDS);
}
protected boolean lockAccount(long accountId) {
final short opendb = TransactionLegacy.currentTxn().getDatabaseId();
boolean success = false;
try (TransactionLegacy txn = TransactionLegacy.open(TransactionLegacy.CLOUD_DB)) {
Account account = _accountDao.findById(accountId);
if (account != null) {
if (account.getState() == State.LOCKED) {
return true; // already locked, no-op
} else if (account.getState() == State.ENABLED) {
AccountVO acctForUpdate = _accountDao.createForUpdate();
acctForUpdate.setState(State.LOCKED);
success = _accountDao.update(Long.valueOf(accountId), acctForUpdate);
} else {
if (s_logger.isInfoEnabled()) {
s_logger.info("Attempting to lock a non-enabled account, current state is " + account.getState() + " (accountId: " + accountId + "), locking failed.");
}
}
} else {
s_logger.warn("Failed to lock account " + accountId + ", account not found.");
}
} catch (Exception e) {
s_logger.error("Exception occurred while locking account by Quota Alert Manager", e);
throw e;
} finally {
TransactionLegacy.open(opendb).close();
}
return success;
}
public static class DeferredQuotaEmail {
private AccountVO account;
private QuotaAccountVO quotaAccount;
private QuotaConfig.QuotaEmailTemplateTypes emailTemplateType;
private BigDecimal quotaUsage;
public DeferredQuotaEmail(AccountVO account, QuotaAccountVO quotaAccount, BigDecimal quotaUsage, QuotaConfig.QuotaEmailTemplateTypes emailTemplateType) {
this.account = account;
this.quotaAccount = quotaAccount;
this.emailTemplateType = emailTemplateType;
this.quotaUsage = quotaUsage;
}
public DeferredQuotaEmail(AccountVO account, QuotaAccountVO quotaAccount, QuotaConfig.QuotaEmailTemplateTypes emailTemplateType) {
this.account = account;
this.quotaAccount = quotaAccount;
this.emailTemplateType = emailTemplateType;
this.quotaUsage = new BigDecimal(-1);
}
public AccountVO getAccount() {
return account;
}
public BigDecimal getQuotaBalance() {
return quotaAccount.getQuotaBalance();
}
public BigDecimal getQuotaUsage() {
return quotaUsage;
}
public Date getSendDate() {
if (emailTemplateType == QuotaEmailTemplateTypes.QUOTA_STATEMENT) {
return quotaAccount.getLastStatementDate();
} else {
return quotaAccount.getQuotaAlertDate();
}
}
public QuotaConfig.QuotaEmailTemplateTypes getEmailTemplateType() {
return emailTemplateType;
}
public void sentSuccessfully(final QuotaAccountDao quotaAccountDao) {
if (emailTemplateType == QuotaEmailTemplateTypes.QUOTA_STATEMENT) {
quotaAccount.setLastStatementDate(new Date());
} else {
quotaAccount.setQuotaAlertDate(new Date());
quotaAccount.setQuotaAlertType(emailTemplateType.ordinal());
}
quotaAccountDao.updateQuotaAccount(quotaAccount.getAccountId(), quotaAccount);
}
};
protected void sendQuotaAlert(String accountUuid, List<String> emails, String subject, String body) {
SMTPMailProperties mailProperties = new SMTPMailProperties();
mailProperties.setSender(new MailAddress(senderAddress));
mailProperties.setSubject(subject);
mailProperties.setContent(body);
mailProperties.setContentType("text/html; charset=utf-8");
if (CollectionUtils.isEmpty(emails)) {
s_logger.warn(String.format("Account [%s] does not have users with email registered, "
+ "therefore we are unable to send quota alert email with subject [%s] and content [%s].", accountUuid, subject, body));
return;
}
Set<MailAddress> addresses = new HashSet<>();
for (String email : emails) {
addresses.add(new MailAddress(email));
}
mailProperties.setRecipients(addresses);
mailSender.sendMail(mailProperties);
}
}