/**
* Copyright (c) 2009--2014 Red Hat, Inc.
*
* This software is licensed to you under the GNU General Public License,
* version 2 (GPLv2). There is NO WARRANTY for this software, express or
* implied, including the implied warranties of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. You should have received a copy of GPLv2
* along with this software; if not, see
* http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt.
*
* Red Hat trademarks are not licensed under GPLv2. No permission is
* granted to use or replicate Red Hat trademarks that are incorporated
* in this software or its documentation.
*/
package com.redhat.rhn.taskomatic.task;
import com.redhat.rhn.common.conf.Config;
import com.redhat.rhn.common.conf.ConfigDefaults;
import com.redhat.rhn.common.db.datasource.ModeFactory;
import com.redhat.rhn.common.db.datasource.SelectMode;
import com.redhat.rhn.common.db.datasource.WriteMode;
import com.redhat.rhn.common.hibernate.HibernateFactory;
import com.redhat.rhn.common.localization.LocalizationService;
import com.redhat.rhn.common.messaging.Mail;
import com.redhat.rhn.common.messaging.SmtpMail;
import com.redhat.rhn.domain.org.OrgFactory;
import com.redhat.rhn.frontend.dto.ActionMessage;
import com.redhat.rhn.frontend.dto.AwolServer;
import com.redhat.rhn.frontend.dto.OrgIdWrapper;
import com.redhat.rhn.frontend.dto.ReportingUser;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.time.StopWatch;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
/**
* DailySummary task.
* sends daily report of stats. reaps org suggestions
* from rhnDailySummaryQueue. Not very "daily" since it runs every
* 30 seconds. Need to look at RHN::DailySummaryEngine. This task
* queues org emails, mails queued emails, then dequeues the emails.
* @version $Rev$
*/
public class DailySummary extends RhnJavaJob {
private static final int HEADER_SPACER = 10;
private static final int ERRATA_SPACER = 4;
private static final String ERRATA_UPDATE = "Errata Update";
private static final String ERRATA_INDENTION = StringUtils.repeat(" ", ERRATA_SPACER);
private Mail mail;
/**
* Default constructor
*/
public DailySummary() {
this(new SmtpMail());
}
/**
* Constructor takes in a Mailer
* @param mailer mailer if you don't want to use the default SmtpMail
*/
public DailySummary(Mail mailer) {
mail = mailer;
}
/**
* {@inheritDoc}
*/
public void execute(JobExecutionContext ctxIn)
throws JobExecutionException {
SelectMode m = ModeFactory.getMode(TaskConstants.MODE_NAME,
TaskConstants.TASK_QUERY_DAILY_SUMMARY_QUEUE);
List results = m.execute();
OrgIdWrapper oiw = null;
for (Iterator itr = results.iterator(); itr.hasNext();) {
try {
oiw = (OrgIdWrapper) itr.next();
if (log.isDebugEnabled()) {
log.debug("dealing with org: " + oiw.toLong());
}
queueOrgEmails(oiw.toLong());
}
catch (Exception e) {
log.error(e.getMessage(), e);
}
finally {
try {
dequeueOrg(oiw.toLong());
if (log.isDebugEnabled()) {
log.debug("org " + oiw.toLong() + " removed from queue");
}
}
finally {
HibernateFactory.commitTransaction();
HibernateFactory.closeSession();
}
}
}
}
/**
* DO NOT CALL FROM OUTSIDE THIS CLASS. Removes the orgs from the queue
* table.
* @param orgId Org Id to be dequeued.
* @return # of orgs dequeued
*/
public int dequeueOrg(Long orgId) {
WriteMode m = ModeFactory.getWriteMode(TaskConstants.MODE_NAME,
TaskConstants.TASK_QUERY_DEQUEUE_DAILY_SUMMARY);
Map<String, Object> params = new HashMap<String, Object>();
params.put("org_id", orgId);
return m.executeUpdate(params);
}
/**
* DO NOT CALL FROM OUTSIDE THIS CLASS. Queues up the Org Emails for
* mailing.
* @param orgId Org Id to be processed.
*/
public void queueOrgEmails(Long orgId) {
SelectMode m = ModeFactory.getMode(TaskConstants.MODE_NAME,
TaskConstants.TASK_QUERY_USERS_WANTING_REPORTS);
Map<String, Object> params = new HashMap<String, Object>();
params.put("org_id", orgId);
StopWatch watch = new StopWatch();
watch.start();
List users = m.execute(params);
for (Iterator itr = users.iterator(); itr.hasNext();) {
ReportingUser ru = (ReportingUser) itr.next();
// run_user
List awol = getAwolServers(ru.idAsLong());
// send email
List actions = getActionInfo(ru.idAsLong());
if ((awol == null || awol.size() == 0) && (actions == null ||
actions.size() == 0)) {
log.debug("Skipping ORG " + orgId + " because daily summary info has " +
"changed");
continue;
}
String awolMsg = renderAwolServersMessage(awol);
String actionMsg = renderActionsMessage(actions);
String emailMsg = prepareEmail(
ru.getLogin(), ru.getAddress(), awolMsg, actionMsg);
LocalizationService ls = LocalizationService.getInstance();
mail.setSubject(ls.getMessage(
"dailysummary.email.subject", ls.formatShortDate(new Date())));
mail.setRecipient(ru.getAddress());
if (log.isDebugEnabled()) {
log.debug("Sending email to [" + ru.getAddress() + "]");
}
mail.setBody(emailMsg);
TaskHelper.sendMail(mail, log);
}
watch.stop();
if (log.isDebugEnabled()) {
log.debug("queued emails of org of " + users.size() +
" users in " + watch.getTime() + "ms");
}
}
/**
* DO NOT CALL FROM OUTSIDE THIS CLASS. Returns the list of awol servers.
* @param uid User id whose awol servers are sought.
* @return the list of recent awol servers.
*/
public List getAwolServers(Long uid) {
SelectMode m = ModeFactory.getMode(TaskConstants.MODE_NAME,
TaskConstants.TASK_QUERY_USERS_AWOL_SERVERS);
Map<String, Object> params = new HashMap<String, Object>();
params.put("user_id", uid);
params.put("checkin_threshold",
Config.get().getInteger(ConfigDefaults.SYSTEM_CHECKIN_THRESHOLD));
return m.execute(params);
}
/**
* DO NOT CALL FROM OUTSIDE THIS CLASS. Returns the list of recent actions.
* @param uid User id whose recent actions are sought.
* @return the list of recent actions.
*/
public List getActionInfo(Long uid) {
SelectMode m = ModeFactory.getMode(TaskConstants.MODE_NAME,
TaskConstants.TASK_QUERY_GET_ACTION_INFO);
Map<String, Object> params = new HashMap<String, Object>();
params.put("user_id", uid);
return m.execute(params);
}
/**
* DO NOT CALL FROM OUTSIDE THIS CLASS. Renders the awol servers message
* @param servers list of awol servers
* @return the awol servers message
*/
public String renderAwolServersMessage(List servers) {
if (servers == null || servers.isEmpty()) {
return "";
}
/*
* The Awol message is going to be a table containing a list of systems
* that have gone AWOL.
*
* All the calculation crap for tables will be done... how many spaces
* between columns and the column width for the given data.
* This means that we will read through the data twice, once to find the
* longest entries and again to build the return string.
*
* Since this will be going in an email, if the receiver doesn't use
* monospace fonts *ever* than all this calculation is for nothing.
*/
LocalizationService ls = LocalizationService.getInstance();
String sid = ls.getMessage("taskomatic.daily.sid"); //System Id column
String sname = ls.getMessage("taskomatic.daily.systemname"); //System Name column
String checkin = ls.getMessage("taskomatic.daily.checkin"); //Last Checkin column
//First we need to figure out how long the width of the columns should be.
int minDiff = 4; //this is the minimum spaces between header elements
int sidLength = sid.length() + minDiff;
int snameLength = sid.length() + minDiff;
//Find the longest entry in the table for both sid and sname.
for (Iterator itr = servers.iterator(); itr.hasNext();) {
AwolServer as = (AwolServer) itr.next();
String currentId = as.getId().toString();
if (currentId.length() >= sidLength) {
//extra space so the longest entry doesn't connect to the next column
sidLength = currentId.length() + 1;
}
String currentName = as.getName();
if (currentName.length() >= snameLength) {
//extra space so the longest entry doesn't connect to the next column
snameLength = currentName.length() + 1;
}
}
//render the header-- System Id System Name LastCheckin
StringBuilder buf = new StringBuilder();
buf.append(sid);
buf.append(StringUtils.repeat(" ", sidLength - sid.length()));
buf.append(sname);
buf.append(StringUtils.repeat(" ", snameLength - sname.length()));
buf.append(checkin);
buf.append("\n");
//Now render the data in the table
for (Iterator itr = servers.iterator(); itr.hasNext();) {
AwolServer as = (AwolServer) itr.next();
String currentId = as.getId().toString();
buf.append(currentId);
buf.append(StringUtils.repeat(" ", sidLength - currentId.length()));
String currentName = as.getName();
buf.append(currentName);
buf.append(StringUtils.repeat(" ", snameLength - currentName.length()));
buf.append(as.getCheckin());
buf.append("\n");
}
//Lastly, create the url for the link in the email.
StringBuilder url = new StringBuilder();
if (Config.get().getBoolean(ConfigDefaults.SSL_AVAILABLE)) {
url.append("https://");
}
else {
url.append("http://");
}
url.append(getHostname());
url.append("/rhn/systems/Inactive.do");
return LocalizationService.getInstance().getMessage(
"taskomatic.msg.awolservers", buf.toString(), url);
}
/**
* DO NOT CALL FROM OUTSIDE THIS CLASS. Renders the actions email message
* @param actions list of recent actions
* @return the actions email message
*/
public String renderActionsMessage(List<ActionMessage> actions) {
int longestActionLength = HEADER_SPACER;
int longestStatusLength = 0;
StringBuilder hdr = new StringBuilder();
StringBuilder body = new StringBuilder();
StringBuilder legend = new StringBuilder();
StringBuilder msg = new StringBuilder();
LinkedHashSet<String> statusSet = new LinkedHashSet();
TreeMap<String, Map<String, Integer>> nonErrataActions = new TreeMap();
TreeMap<String, Map<String, Integer>> errataActions = new TreeMap();
TreeMap<String, String> errataSynopsis = new TreeMap();
legend.append(LocalizationService
.getInstance().getMessage("taskomatic.daily.errata"));
legend.append("\n\n");
for (ActionMessage am : actions) {
if (!statusSet.contains(am.getStatus())) {
statusSet.add(am.getStatus());
if (am.getStatus().length() > longestStatusLength) {
longestStatusLength = am.getStatus().length();
}
}
if (am.getType().equals(ERRATA_UPDATE)) {
String advisoryKey = ERRATA_INDENTION + am.getAdvisory();
if (!errataActions.containsKey(advisoryKey)) {
errataActions.put(advisoryKey, new HashMap());
if (advisoryKey.length() + HEADER_SPACER > longestActionLength) {
longestActionLength = advisoryKey.length() + HEADER_SPACER;
}
}
Map<String, Integer> counts = errataActions.get(advisoryKey);
counts.put(am.getStatus(), am.getCount());
if (am.getAdvisory() != null &&
!errataSynopsis.containsKey(am.getAdvisory())) {
errataSynopsis.put(am.getAdvisory(), am.getSynopsis());
}
}
else {
if (!nonErrataActions.containsKey(am.getType())) {
nonErrataActions.put(am.getType(), new HashMap());
if (am.getType().length() + HEADER_SPACER > longestActionLength) {
longestActionLength = am.getType().length() + HEADER_SPACER;
}
}
Map<String, Integer> counts = nonErrataActions.get(am.getType());
counts.put(am.getStatus(), am.getCount());
}
}
hdr.append(StringUtils.repeat(" ", longestActionLength));
for (String status : statusSet) {
hdr.append(status + StringUtils.repeat(" ", (longestStatusLength +
ERRATA_SPACER) - status.length()));
}
if (!errataActions.isEmpty()) {
body.append(ERRATA_UPDATE + ":" + "\n");
}
StringBuffer formattedErrataActions = renderActionTree(longestActionLength,
longestStatusLength, statusSet, errataActions);
body.append(formattedErrataActions);
for (String advisory : errataSynopsis.keySet()) {
legend.append(ERRATA_INDENTION + advisory + ERRATA_INDENTION +
errataSynopsis.get(advisory) + "\n");
}
StringBuffer formattedNonErrataActions = renderActionTree(longestActionLength,
longestStatusLength, statusSet, nonErrataActions);
body.append(formattedNonErrataActions);
// finally put all this together
msg.append(hdr.toString());
msg.append("\n");
msg.append(body.toString());
msg.append("\n\n");
if (!errataSynopsis.isEmpty()) {
msg.append(legend.toString());
}
return msg.toString();
}
private StringBuffer renderActionTree(int longestActionLength,
int longestStatusLength, LinkedHashSet<String> statusSet,
TreeMap<String, Map<String, Integer>> actionTree) {
StringBuffer formattedActions = new StringBuffer();
for (String actionName : actionTree.keySet()) {
formattedActions.append(actionName +
StringUtils.repeat(" ", (longestActionLength - (actionName.length()))));
for (String status : statusSet) {
Map<String, Integer> counts = actionTree.get(actionName);
Integer theCount = counts.get(status);
if (counts.containsKey(status)) {
theCount = counts.get(status);
}
else {
theCount = 0;
}
formattedActions.append(theCount);
formattedActions.append(StringUtils.repeat(" ", longestStatusLength +
ERRATA_SPACER - theCount.toString().length()));
}
formattedActions.append("\n");
}
return formattedActions;
}
/**
* DO NOT CALL FROM OUTSIDE THIS CLASS. Prepares the email message string
* @param login users login
* @param email email address
* @param awolMsg the awol servers msg
* @param actionMsg the recent actions message
* @return the email message string
*/
public String prepareEmail(
String login, String email, String awolMsg, String actionMsg) {
LocalizationService ls = LocalizationService.getInstance();
String[] args = new String[7];
args[0] = login;
args[1] = ls.formatDate(new Date());
args[2] = actionMsg;
args[3] = awolMsg;
args[4] = getHostname();
// why the hell are these in OrgFactory?
args[5] = OrgFactory.EMAIL_FOOTER.getValue();
args[6] = OrgFactory.EMAIL_ACCOUNT_INFO.getValue();
String msg = ls.getMessage(
"dailysummary.email.body", (Object[])args);
// wow, what an ugly @$$ hack, but this requires rewriting
// the email templating engine which kinda sucks.
msg = StringUtils.replace(msg, "<login />", login);
return StringUtils.replace(msg, "<email-address />", email);
}
private String getHostname() {
return ConfigDefaults.get().getHostname();
}
}