blob: 942aeb9b8f0a8ad54fb7c8416be1fb5f129f9d82 [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.io.UnsupportedEncodingException;
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.Properties;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import javax.mail.Authenticator;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.PasswordAuthentication;
import javax.mail.Session;
import javax.mail.URLName;
import javax.mail.internet.InternetAddress;
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.lang3.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.NumbersUtil;
import com.cloud.utils.component.ManagerBase;
import com.cloud.utils.db.TransactionLegacy;
import com.google.common.base.Strings;
import com.sun.mail.smtp.SMTPMessage;
import com.sun.mail.smtp.SMTPSSLTransport;
import com.sun.mail.smtp.SMTPTransport;
@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 EmailQuotaAlert _emailQuotaAlert;
private boolean _lockAccountEnforcement = false;
boolean _smtpDebug = false;
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);
}
final String smtpHost = configs.get(QuotaConfig.QuotaSmtpHost.key());
int smtpPort = NumbersUtil.parseInt(configs.get(QuotaConfig.QuotaSmtpPort.key()), 25);
String useAuthStr = configs.get(QuotaConfig.QuotaSmtpAuthType.key());
boolean useAuth = ((useAuthStr != null) && Boolean.parseBoolean(useAuthStr));
String smtpUsername = configs.get(QuotaConfig.QuotaSmtpUser.key());
String smtpPassword = configs.get(QuotaConfig.QuotaSmtpPassword.key());
String emailSender = configs.get(QuotaConfig.QuotaSmtpSender.key());
_lockAccountEnforcement = "true".equalsIgnoreCase(configs.get(QuotaConfig.QuotaEnableEnforcement.key()));
_emailQuotaAlert = new EmailQuotaAlert(smtpHost, smtpPort, useAuth, smtpUsername, smtpPassword, emailSender, _smtpDebug);
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> optionMap = new HashMap<String, String>();
optionMap.put("accountName", account.getAccountName());
optionMap.put("accountID", account.getUuid());
optionMap.put("accountUsers", userNames);
optionMap.put("domainName", accountDomain.getName());
optionMap.put("domainID", accountDomain.getUuid());
optionMap.put("quotaBalance", QuotaConfig.QuotaCurrencySymbol.value() + " " + balance.toString());
if (emailType == QuotaEmailTemplateTypes.QUOTA_STATEMENT) {
optionMap.put("quotaUsage", QuotaConfig.QuotaCurrencySymbol.value() + " " + usage.toString());
}
if (s_logger.isDebugEnabled()) {
s_logger.debug("accountName" + account.getAccountName() + "accountID" + account.getUuid() + "accountUsers" + userNames + "domainName" + accountDomain.getName() + "domainID"
+ accountDomain.getUuid());
}
final StrSubstitutor templateEngine = new StrSubstitutor(optionMap);
final String subject = templateEngine.replace(emailTemplate.getTemplateSubject());
final String body = templateEngine.replace(emailTemplate.getTemplateBody());
try {
_emailQuotaAlert.sendQuotaAlert(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 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 occured 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);
}
};
static class EmailQuotaAlert {
private final Session _smtpSession;
private final String _smtpHost;
private final int _smtpPort;
private final boolean _smtpUseAuth;
private final String _smtpUsername;
private final String _smtpPassword;
private final String _emailSender;
public EmailQuotaAlert(String smtpHost, int smtpPort, boolean smtpUseAuth, final String smtpUsername, final String smtpPassword, String emailSender, boolean smtpDebug) {
_smtpHost = smtpHost;
_smtpPort = smtpPort;
_smtpUseAuth = smtpUseAuth;
_smtpUsername = smtpUsername;
_smtpPassword = smtpPassword;
_emailSender = emailSender;
if (!Strings.isNullOrEmpty(_smtpHost)) {
Properties smtpProps = new Properties();
smtpProps.put("mail.smtp.host", smtpHost);
smtpProps.put("mail.smtp.port", smtpPort);
smtpProps.put("mail.smtp.auth", "" + smtpUseAuth);
if (smtpUsername != null) {
smtpProps.put("mail.smtp.user", smtpUsername);
}
smtpProps.put("mail.smtps.host", smtpHost);
smtpProps.put("mail.smtps.port", smtpPort);
smtpProps.put("mail.smtps.auth", "" + smtpUseAuth);
if (!Strings.isNullOrEmpty(smtpUsername)) {
smtpProps.put("mail.smtps.user", smtpUsername);
}
if (!Strings.isNullOrEmpty(smtpUsername) && !Strings.isNullOrEmpty(smtpPassword)) {
_smtpSession = Session.getInstance(smtpProps, new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(smtpUsername, smtpPassword);
}
});
} else {
_smtpSession = Session.getInstance(smtpProps);
}
_smtpSession.setDebug(smtpDebug);
} else {
_smtpSession = null;
}
}
public void sendQuotaAlert(List<String> emails, String subject, String body) throws MessagingException, UnsupportedEncodingException {
if (_smtpSession == null) {
s_logger.error("Unable to create smtp session.");
return;
}
SMTPMessage msg = new SMTPMessage(_smtpSession);
msg.setSender(new InternetAddress(_emailSender, _emailSender));
msg.setFrom(new InternetAddress(_emailSender, _emailSender));
for (String email : emails) {
if (email != null && !email.isEmpty()) {
try {
InternetAddress address = new InternetAddress(email, email);
msg.addRecipient(Message.RecipientType.TO, address);
} catch (Exception pokemon) {
s_logger.error("Exception in creating address for:" + email, pokemon);
}
}
}
msg.setSubject(subject);
msg.setSentDate(new Date());
msg.setContent(body, "text/html; charset=utf-8");
msg.saveChanges();
SMTPTransport smtpTrans = null;
if (_smtpUseAuth) {
smtpTrans = new SMTPSSLTransport(_smtpSession, new URLName("smtp", _smtpHost, _smtpPort, null, _smtpUsername, _smtpPassword));
} else {
smtpTrans = new SMTPTransport(_smtpSession, new URLName("smtp", _smtpHost, _smtpPort, null, _smtpUsername, _smtpPassword));
}
smtpTrans.connect();
smtpTrans.sendMessage(msg, msg.getAllRecipients());
smtpTrans.close();
}
}
}