/* * NOTE: This copyright does *not* cover user programs that use Hyperic * program services by normal system calls through the application * program interfaces provided as part of the Hyperic Plug-in Development * Kit or the Hyperic Client Development Kit - this is merely considered * normal use of the program, and does *not* fall under the heading of * "derived work". * * Copyright (C) [2004-2010], VMware, Inc. * This file is part of Hyperic. * * Hyperic is free software; you can redistribute it and/or modify * it under the terms version 2 of the GNU General Public License as * published by the Free Software Foundation. This program is distributed * in the hope that it will be useful, but WITHOUT ANY WARRANTY; without * even the implied warranty of MERCHANTABILITY or FITNESS FOR A * PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * USA. */ package org.hyperic.hq.bizapp.server.action.email; import java.io.File; import java.io.StringWriter; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Properties; import java.util.ResourceBundle; import java.util.Set; import javax.mail.internet.AddressException; import javax.mail.internet.InternetAddress; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.hyperic.hq.appdef.shared.AppdefEntityID; import org.hyperic.hq.appdef.shared.AppdefUtil; import org.hyperic.hq.application.Scheduler; import org.hyperic.hq.authz.server.session.AuthzSubject; import org.hyperic.hq.authz.server.session.Resource; import org.hyperic.hq.authz.server.session.ResourceDAO; import org.hyperic.hq.authz.shared.AuthzSubjectManager; import org.hyperic.hq.bizapp.shared.EmailManager; import org.hyperic.hq.bizapp.shared.action.EmailActionConfig; import org.hyperic.hq.common.shared.ServerConfigManager; import org.hyperic.hq.context.Bootstrap; import org.hyperic.hq.escalation.server.session.Escalatable; import org.hyperic.hq.escalation.server.session.EscalationStateChange; import org.hyperic.hq.escalation.server.session.PerformsEscalations; import org.hyperic.hq.events.ActionExecuteException; import org.hyperic.hq.events.ActionExecutionInfo; import org.hyperic.hq.events.ActionInterface; import org.hyperic.hq.events.AlertDefinitionInterface; import org.hyperic.hq.events.AlertInterface; import org.hyperic.hq.events.EventConstants; import org.hyperic.hq.events.InvalidActionDataException; import org.hyperic.hq.events.Notify; import org.hyperic.hq.events.server.session.AlertRegulator; import org.hyperic.hq.hqu.RenditServer; import org.hyperic.hq.measurement.MeasurementNotFoundException; import org.hyperic.hq.stats.ConcurrentStatsWriter; import org.hyperic.util.ConfigPropertyException; import org.hyperic.util.StringUtil; import org.hyperic.util.config.ConfigResponse; public class EmailAction extends EmailActionConfig implements ActionInterface, Notify { protected static String baseUrl = null; private static final int _alertThreshold; private static final List<EmailObj> _emails = new ArrayList<EmailObj>(); private static EmailRecipient[] _emailAddrs; // Evaluate number of notifications each period when AlertThreshold is // enabled to potentially toggle/block all notifications for 1 - Many // THRESHOLD_WINDOW(s). Notifications are only sent out after each period // when mechanism is turned on. // Matched it up with ConcurrentStatsCollector in order to obtain easy // stats on a deployment's activity private static final long EVALUATION_PERIOD = ConcurrentStatsWriter.WRITE_PERIOD*1000; // evaluation window to continue and block notifications or // toggle back to regular operation private static final int THRESHOLD_WINDOW = 10*60*1000; private static final Log _log = LogFactory.getLog(EmailAction.class); private static final String BUNDLE = "org.hyperic.hq.bizapp.Resources"; private ResourceDAO resourceDAO = Bootstrap.getBean(ResourceDAO.class); static { ServerConfigManager sConf = Bootstrap.getBean(ServerConfigManager.class); int tmp = 0; try { final Properties props = sConf.getConfig(); tmp = Integer.parseInt(props.getProperty("HQ_ALERT_THRESHOLD", "0")); String[] array = props.getProperty("HQ_ALERT_THRESHOLD_EMAILS", "").split(","); _emailAddrs = new EmailRecipient[array.length]; for (int i=0; i<array.length; i++) { try { _emailAddrs[i] = new EmailRecipient(new InternetAddress(array[i]), false); } catch (AddressException e) { _log.debug(e.getMessage(), e); } } if (_emailAddrs.length == 0) { tmp = 0; } } catch (NumberFormatException e) { _log.debug(e.getMessage(), e); } catch (ConfigPropertyException e) { _log.debug(e.getMessage(), e); } _alertThreshold = tmp; if (_alertThreshold > 0) { Bootstrap.getBean(Scheduler.class).scheduleWithFixedDelay( new ThresholdWorker(), Scheduler.NO_INITIAL_DELAY, EVALUATION_PERIOD); } } public EmailAction() {} protected final AuthzSubjectManager getSubjMan() { return Bootstrap.getBean(AuthzSubjectManager.class); } private String renderTemplate(String filename, Map<?,?> params) { StringWriter output = new StringWriter(); try { File templateDir = Bootstrap.getResource("WEB-INF/alertTemplates").getFile(); File templateFile = new File(templateDir, filename); Bootstrap.getBean(RenditServer.class).renderTemplate(templateFile, params, output); if (_log.isDebugEnabled()) { _log.debug("Template rendered\n" + output.toString()); } } catch(Exception e) { _log.warn("Unable to render template", e); } return output.toString(); } private String createSubject(AlertDefinitionInterface alertdef, AlertInterface alert, Resource resource, ActionExecutionInfo action, String status) { Map<String, Object> params = new HashMap<String, Object>(); params.put("resource", resource); params.put("alertDef", alertdef); params.put("alert", alert); params.put("action", action); params.put("status", status); params.put("isSms", new Boolean(isSms())); return renderTemplate("subject.gsp", params); } private String createText(AlertDefinitionInterface alertdef, ActionExecutionInfo info, Resource resource, AlertInterface alert, String templateName, AuthzSubject user) throws MeasurementNotFoundException { Map<String, Object> params = new HashMap<String, Object>(); params.put("alertDef", alertdef); params.put("alert", alert); params.put("action", info); params.put("resource", resource); params.put("user", user); return renderTemplate(templateName, params); } private AppdefEntityID getResource(AlertDefinitionInterface def) { return AppdefUtil.newAppdefEntityId(def.getResource()); } public String execute(AlertInterface alert, ActionExecutionInfo info) throws ActionExecuteException { try { if (!Bootstrap.getBean(AlertRegulator.class).alertNotificationsAllowed()) { return ResourceBundle.getBundle(BUNDLE).getString("action.email.error.notificationDisabled"); } Map<EmailRecipient, AuthzSubject> addrs = lookupEmailAddr(); if (addrs.isEmpty()) { return ResourceBundle.getBundle(BUNDLE) .getString("action.email.error.noEmailAddress"); } AlertDefinitionInterface alertDef = alert.getAlertDefinitionInterface(); AppdefEntityID appEnt = getResource(alertDef); String logStr = "No notifications sent, see server log for details."; if (appEnt != null) { Resource resource = alertDef.getResource(); if (resource != null && !resource.isInAsyncDeleteState()) { String[] body = new String[addrs.size()]; String[] htmlBody = new String[addrs.size()]; EmailRecipient[] to = (EmailRecipient[]) addrs.keySet().toArray(new EmailRecipient[addrs.size()]); for (int i = 0; i < to.length; i++) { AuthzSubject user = (AuthzSubject) addrs.get(to[i]); if (to[i].useHtml()) { htmlBody[i] = createText(alertDef, info, resource, alert, "html_email.gsp", user); } body[i] = createText(alertDef, info, resource, alert, isSms() ? "sms_email.gsp" : "text_email.gsp", user); } final String subject = createSubject(alertDef, alert, resource, info, ""); sendAlert(appEnt, to, subject, body, htmlBody, alertDef.getPriority(), alertDef.isNotifyFiltered()); StringBuffer result = getLog(to); logStr = result.toString(); } else { _log.warn("No resource for alert definition " + alertDef.getId() + ", perhaps the resource was deleted? Email notification will not be sent."); } } else { _log.warn("No appdef entity ID for alert definition " + alertDef.getId() + ", perhaps the related platform was deleted? Email notification will not be sent."); } return logStr; } catch (Exception e) { throw new ActionExecuteException(e); } } protected StringBuffer getLog(EmailRecipient[] to) { StringBuffer result = new StringBuffer(isSms() ? "SMS" : "Notified"); // XXX: Should get this strings into a resource file switch (getType()) { case TYPE_USERS : result.append(" users: "); break; default : case TYPE_EMAILS : result.append(": "); break; } for (int i = 0; i < to.length; i++) { result.append(to[i].getAddress().getPersonal()); if (i < to.length - 1) { result.append(", "); } } return result; } protected Map<EmailRecipient, AuthzSubject> lookupEmailAddr() throws ActionExecuteException { // First, look up the addresses HashSet<InternetAddress> prevRecipients = new HashSet<InternetAddress>(); Map<EmailRecipient, AuthzSubject> validRecipients = new HashMap<EmailRecipient, AuthzSubject>(); for (Iterator<?> it = getUsers().iterator(); it.hasNext(); ) { try { InternetAddress addr; boolean useHtml = false; AuthzSubject who = null; switch (getType()) { case TYPE_USERS: Integer uid = (Integer) it.next(); who = getSubjMan().getSubjectById(uid); if (who == null) { _log.warn("User not found: " + uid); continue; } addr = (isSms()) ? new InternetAddress(who.getSMSAddress()) : new InternetAddress(who.getEmailAddress()); addr.setPersonal(who.getName()); useHtml = isSms() ? false : who.isHtmlEmail(); break; default: case TYPE_EMAILS: addr = new InternetAddress((String) it.next(), true); addr.setPersonal(addr.getAddress()); break; } // Don't send duplicate notifications if (prevRecipients.add(addr)) { validRecipients.put(new EmailRecipient(addr, useHtml), who); } } catch (AddressException e) { _log.warn("Mail address invalid", e); continue; } catch (UnsupportedEncodingException e) { _log.warn("Username encoding error", e); continue; } catch (Exception e) { _log.warn("Email lookup failed"); _log.debug("Email lookup failed", e); continue; } } return validRecipients; } public void setParentActionConfig(AppdefEntityID ent, ConfigResponse cfg) throws InvalidActionDataException { init(cfg); } public void send(Escalatable alert, EscalationStateChange change, String message, Set<InternetAddress> notified) throws ActionExecuteException { PerformsEscalations def = alert.getDefinition(); Map<EmailRecipient, AuthzSubject> addrs = lookupEmailAddr(); for (Iterator<Entry<EmailRecipient, AuthzSubject>> it=addrs.entrySet().iterator(); it.hasNext();) { Entry<EmailRecipient, AuthzSubject> entry = it.next(); EmailRecipient rec = entry.getKey(); // Don't notify again if already notified if (notified.contains(rec.getAddress())) { it.remove(); continue; } rec.setHtml(false); notified.add(rec.getAddress()); } AlertDefinitionInterface defInfo = def.getDefinitionInfo(); String[] messages = new String[addrs.size()]; Arrays.fill(messages, message); EmailRecipient[] to = (EmailRecipient[]) addrs.keySet().toArray(new EmailRecipient[addrs.size()]); AppdefEntityID appEnt = getResource(defInfo); Resource resource = resourceDAO.findByInstanceId(appEnt.getAuthzTypeId(), appEnt.getId()); final String subject = createSubject( defInfo, alert.getAlertInfo(), resource, null, change.getDescription()); sendAlert(getResource(defInfo), to, subject, messages, messages, defInfo.getPriority(), false); } private static void sendAlert(AppdefEntityID appEnt, EmailRecipient[] to, String subject, String[] body, String[] htmlBody, int priority, boolean notifyFiltered) { if (_alertThreshold <= 0) { final boolean debug = _log.isDebugEnabled(); if (debug) { EmailObj obj = new EmailObj(appEnt, to, subject, body, htmlBody, priority, notifyFiltered); debug(obj); } getEmailMan().sendAlert(appEnt, to, subject, body, htmlBody, priority, notifyFiltered); } else { synchronized (_emails) { EmailObj obj = new EmailObj(appEnt, to, subject, body, htmlBody, priority, notifyFiltered); _emails.add(obj); } } } private static final EmailManager getEmailMan() { return Bootstrap.getBean(EmailManager.class); } private static final long now() { return System.currentTimeMillis(); } private static void debug(EmailObj obj) { final boolean debug = _log.isDebugEnabled(); if (debug) { final String msg = "Sending alert with info -> " + obj.getAppEnt().getID() + ':' + StringUtil.implode(Arrays.asList(obj.getTo()), ",") + ':' + obj.getSubject() + ':' + obj.getPriority(); _log.debug(msg); } } private static class ThresholdWorker implements Runnable { private long _lastEmailTime = -1l; private boolean _inThresholdWindow = false; private String _endMsg = null, _beginMsg = null, _continueMsg = null, _endSubject = null, _beginSubject = null, _continueSubject = null; public synchronized void run() { try { List<EmailObj> toEmail = null; synchronized(_emails) { if (_emails.size() == 0) { return; } toEmail = new ArrayList<EmailObj>(_emails); _emails.clear(); } if (!_inThresholdWindow) { _inThresholdWindow = (toEmail.size() >= _alertThreshold) ? true : false; } if (_inThresholdWindow && lastEmailWithinThresholdWindow()) { // if we are already in a threshold window then there is // nothing to do until the window has ended // just drop all emails if (_log.isDebugEnabled()) { _log.debug("In Threshold Window, dropping " + toEmail.size() + " email(s)"); } return; } else if (_inThresholdWindow && _lastEmailTime == -1l) { _lastEmailTime = now(); sendRollupEmail(true, toEmail.size()); return; } else if (_inThresholdWindow && !lastEmailWithinThresholdWindow()) { // this means that the threshold window has ended // Need to send an email notifying the users that it has // ended -OR- that it will continue _inThresholdWindow = (toEmail.size() >= _alertThreshold) ? true : false; sendRollupEmail(false, toEmail.size()); _lastEmailTime = _inThresholdWindow ? now() : -1l; return; } else { // send all emails, alert storm is not in affect for (final EmailObj obj : toEmail) { sendFilteredEmail(obj); } return; } } catch (Throwable e) { _log.error(e.getMessage(), e); return; } } private void sendFilteredEmail(EmailObj obj) { debug(obj); getEmailMan().sendAlert(obj.getAppEnt(), obj.getTo(), obj.getSubject(), obj.getBody(), obj.getHtmlBody(), obj.getPriority(), obj.isNotifyFiltered()); } private final boolean lastEmailWithinThresholdWindow() { return (_lastEmailTime > (now() - THRESHOLD_WINDOW)) ? true : false; } private final void sendRollupEmail(boolean startWindow, int notificationCount) throws AddressException { String msg = "", subject = ""; if (startWindow) { msg = getWindowStartMsg(notificationCount); subject = getWindowStartSubject(); } else if (_inThresholdWindow) { msg = getWindowContinueMsg(notificationCount); subject = getWindowContinueSubject(); } else { msg = getWindowEndMsg(); subject = getWindowEndSubject(); } final EmailRecipient[] recipients = getEmailRecipients(); final String[] message = new String[recipients.length]; for (int i=0; i<recipients.length; i++) { message[i] = msg; } if (_log.isDebugEnabled()) { _log.debug("Sending Threshold Email to " + StringUtil.implode(Arrays.asList(recipients), ",") + ',' + " msg: " + msg); } getEmailMan().sendEmail(recipients, subject, message, message, new Integer(EventConstants.PRIORITY_HIGH)); } private final String getWindowEndSubject() { if (_endSubject != null) { return _endSubject; } _endSubject = ResourceBundle.getBundle(BUNDLE).getString( "alert.threshold.subject.end.message"); return _endSubject; } private final String getWindowContinueSubject() { if (_continueSubject != null) { return _continueSubject; } _continueSubject = ResourceBundle.getBundle(BUNDLE).getString( "alert.threshold.subject.continue.message"); return _continueSubject; } private final String getWindowStartSubject() { if (_beginSubject != null) { return _beginSubject; } _beginSubject = ResourceBundle.getBundle(BUNDLE).getString( "alert.threshold.subject.begin.message"); return _beginSubject; } private final String getWindowEndMsg() { if (_endMsg != null) { return _endMsg; } _endMsg = ResourceBundle.getBundle(BUNDLE).getString( "alert.threshold.end.message"); return _endMsg; } private final String getWindowContinueMsg(int notificationCount) { if (_continueMsg != null) { return _continueMsg.replaceAll("\\{0\\}", notificationCount+""); } _continueMsg = ResourceBundle.getBundle(BUNDLE).getString( "alert.threshold.continue.message"); _continueMsg = _continueMsg.replaceAll("\\{1\\}", EVALUATION_PERIOD/1000+"") .replaceAll("\\{2\\}", THRESHOLD_WINDOW/60000+""); return _continueMsg.replaceAll("\\{0\\}", notificationCount+""); } private final String getWindowStartMsg(int notificationCount) { if (_beginMsg != null) { return _beginMsg.replaceAll("\\{0\\}", notificationCount+"") .replaceAll("\\{2\\}", _alertThreshold+""); } _beginMsg = ResourceBundle.getBundle(BUNDLE).getString( "alert.threshold.begin.message"); _beginMsg = _beginMsg.replaceAll("\\{1\\}", EVALUATION_PERIOD/1000+"") .replaceAll("\\{3\\}", THRESHOLD_WINDOW/60000+""); return _beginMsg.replaceAll("\\{0\\}", notificationCount+"") .replaceAll("\\{2\\}", _alertThreshold+""); } private EmailRecipient[] getEmailRecipients() throws AddressException { return _emailAddrs; } } private static class EmailObj { private final AppdefEntityID _appEnt; private final EmailRecipient[] _to; private final String _subject; private final String[] _body; private final String[] _htmlBody; private final int _priority; private final boolean _notifyFiltered; public EmailObj(AppdefEntityID appEnt, EmailRecipient[] to, String subject, String[] body, String[] htmlBody, int priority, boolean notifyFiltered) { _appEnt = appEnt; _to = to; _subject = subject; _body = body; _htmlBody = htmlBody; _priority = priority; _notifyFiltered = notifyFiltered; } public AppdefEntityID getAppEnt() { return _appEnt; } public EmailRecipient[] getTo() { return _to; } public String getSubject() { return _subject; } public String[] getBody() { return _body; } public String[] getHtmlBody() { return _htmlBody; } public int getPriority() { return _priority; } public boolean isNotifyFiltered() { return _notifyFiltered; } } }