/*
* NOTE: This copyright does *not* cover user programs that use HQ
* 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-2009], Hyperic, Inc.
* This file is part of HQ.
*
* HQ 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.control.server.session;
import java.text.ParseException;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.ObjectNotFoundException;
import org.hyperic.hq.appdef.shared.AppdefEntityID;
import org.hyperic.hq.appdef.shared.AppdefEntityNotFoundException;
import org.hyperic.hq.appdef.shared.AppdefEntityValue;
import org.hyperic.hq.authz.server.session.AuthzSubject;
import org.hyperic.hq.authz.shared.PermissionException;
import org.hyperic.hq.authz.shared.PermissionManager;
import org.hyperic.hq.common.ApplicationException;
import org.hyperic.hq.common.NotFoundException;
import org.hyperic.hq.common.SystemException;
import org.hyperic.hq.control.shared.ControlConstants;
import org.hyperic.hq.control.shared.ControlFrequencyValue;
import org.hyperic.hq.control.shared.ControlScheduleManager;
import org.hyperic.hq.control.shared.ScheduledJobNotFoundException;
import org.hyperic.hq.control.shared.ScheduledJobRemoveException;
import org.hyperic.hq.grouping.server.session.GroupUtil;
import org.hyperic.hq.grouping.shared.GroupNotCompatibleException;
import org.hyperic.hq.product.PluginException;
import org.hyperic.hq.scheduler.ScheduleParseException;
import org.hyperic.hq.scheduler.ScheduleParser;
import org.hyperic.hq.scheduler.ScheduleValue;
import org.hyperic.hq.scheduler.ScheduleWillNeverFireException;
import org.hyperic.hq.scheduler.server.session.BaseScheduleManager;
import org.hyperic.util.StringUtil;
import org.hyperic.util.jdbc.DBUtil;
import org.hyperic.util.pager.PageControl;
import org.hyperic.util.pager.PageList;
import org.hyperic.util.pager.SortAttribute;
import org.hyperic.util.timer.StopWatch;
import org.quartz.CronTrigger;
import org.quartz.JobDataMap;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.SimpleTrigger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* Control manager for dealing with scheduled actions
*
*/
@Service
public class ControlScheduleManagerImpl
extends BaseScheduleManager implements ControlScheduleManager {
private static final int MAX_HISTORY_TEXT_SIZE = 500;
private Log log = LogFactory.getLog(ControlScheduleManagerImpl.class);
private static final String JOB_PREFIX = "action";
private static final String SCHEDULE_PREFIX = "interval";
private static final String GROUP = "control";
private static final String PAGER_BASE = "org.hyperic.hq.control.server.session.";
private static final String HISTORY_PAGER = PAGER_BASE + "PagerProcessor_control_history";
private static final String SCHEDULE_PAGER = PAGER_BASE + "PagerProcessor_control_schedule";
private ControlHistoryDAO controlHistoryDAO;
private ControlScheduleDAO controlScheduleDAO;
private PermissionManager permissionManager;
@Autowired
public ControlScheduleManagerImpl(Scheduler scheduler, DBUtil dbUtil, ControlHistoryDAO controlHistoryDAO,
ControlScheduleDAO controlScheduleDAO,
PermissionManager permissionManager) {
super(scheduler, dbUtil);
this.controlHistoryDAO = controlHistoryDAO;
this.controlScheduleDAO = controlScheduleDAO;
this.permissionManager = permissionManager;
}
protected String getHistoryPagerClass() {
return HISTORY_PAGER;
}
protected String getSchedulePagerClass() {
return SCHEDULE_PAGER;
}
protected String getJobPrefix() {
return JOB_PREFIX;
}
protected String getSchedulePrefix() {
return SCHEDULE_PREFIX;
}
protected void setupJobData(JobDetail jobDetail, AuthzSubject subject, AppdefEntityID id, String action,
String args, String scheduleString, Boolean scheduled, String description, int[] order) {
super.setupJobData(jobDetail, subject, id, scheduleString, scheduled, order);
JobDataMap dataMap = jobDetail.getJobDataMap();
// Quartz 1.5 requires Strings in the JobDataMap to be non-null
if (description == null) {
description = "";
}
if (args == null) {
args = "";
}
dataMap.put(ControlJob.PROP_DESCRIPTION, description);
dataMap.put(ControlJob.PROP_ACTION, action);
dataMap.put(ControlJob.PROP_ARGS, args);
}
/**
* Get a list of recent control actions in decending order
*
*
*
*/
@Transactional(readOnly=true)
public PageList<ControlHistory> getRecentControlActions(AuthzSubject subject, int rows, long window)
throws ApplicationException {
StopWatch watch = new StopWatch();
// this routine ignores sort attribute!
try {
Collection<ControlHistory> recent = controlHistoryDAO.findByStartTime(System.currentTimeMillis() - window,
false);
// Run through the list only returning entities the user
// has the ability to see
int count = 0;
for (Iterator<ControlHistory> i = recent.iterator(); i.hasNext();) {
ControlHistory cLocal = i.next();
AppdefEntityID entity = new AppdefEntityID(cLocal.getEntityType().intValue(), cLocal.getEntityId());
try {
checkControlPermission(subject, entity);
AppdefEntityValue aVal = new AppdefEntityValue(entity, subject);
cLocal.setEntityName(aVal.getName());
if (++count > rows)
break;
} catch (PermissionException e) {
i.remove();
} catch (AppdefEntityNotFoundException e) {
// Resource not found, skip it and move on
i.remove();
}
}
PageList<ControlHistory> list = historyPager.seek(recent, 0, rows);
list.setTotalSize(recent.size());
return list;
} catch (ObjectNotFoundException e) {
throw new ApplicationException(e);
} finally {
if (log.isDebugEnabled())
log.debug("getRecentControlActions(): " + watch.getElapsed());
}
}
/**
* Get a list of pending control actions in decending order
*
*
*
*/
@Transactional(readOnly=true)
public PageList<ControlSchedule> getPendingControlActions(AuthzSubject subject, int rows)
throws ApplicationException {
StopWatch watch = new StopWatch();
// this routine ignores sort attribute!
try {
Collection<ControlSchedule> pending = controlScheduleDAO.findByFireTime(false);
// Run through the list only returning entities the user
// has the ability to see
int count = 0;
for (Iterator<ControlSchedule> i = pending.iterator(); i.hasNext();) {
ControlSchedule sLocal = i.next();
AppdefEntityID entity = new AppdefEntityID(sLocal.getEntityType().intValue(), sLocal.getEntityId());
try {
checkControlPermission(subject, entity);
if (++count > rows)
break;
} catch (PermissionException e) {
i.remove();
}
}
// This will remove stale data and update fire times which
// may result in the list being out of order. We should
// probably sort a second time
PageList<ControlSchedule> list = schedulePager.seek(pending, 0, rows);
return list;
} finally {
if (log.isDebugEnabled())
log.debug("getPendingControlActions(): " + watch.getElapsed());
}
}
/**
* Get a list of most active control operations
*
*
*
*
* XXX: This could also take a page control, although we would ignore
* everything except for the size
*/
@Transactional(readOnly=true)
public PageList<ControlFrequencyValue> getOnDemandControlFrequency(AuthzSubject subject, int numToReturn)
throws ApplicationException {
PageList<ControlFrequencyValue> list = new PageList<ControlFrequencyValue>();
try {
List<ControlFrequency> frequencies = controlHistoryDAO.getControlFrequencies(numToReturn);
for (ControlFrequency frequency: frequencies) {
try {
checkControlPermission(subject, frequency.getId());
AppdefEntityValue aVal = new AppdefEntityValue(frequency.getId(), subject);
String name = aVal.getName();
ControlFrequencyValue cv =
new ControlFrequencyValue(name, frequency.getId().getType(), frequency.getId().getID(),
frequency.getAction(), (int)frequency.getCount());
list.add(cv);
} catch (AppdefEntityNotFoundException e) {
log.debug(e,e);
continue;
} catch (PermissionException e) {
log.debug(e,e);
continue;
}
}
} catch (Exception e) {
throw new ApplicationException(e);
}
return list;
}
/**
* Get a list of scheduled jobs based on appdef id
*
*
*
*/
@Transactional(readOnly=true)
public PageList<ControlSchedule> findScheduledJobs(AuthzSubject subject, AppdefEntityID id, PageControl pc)
throws ScheduledJobNotFoundException {
Collection<ControlSchedule> schedule;
try {
// default the sorting to the next fire time
pc = PageControl.initDefaults(pc, SortAttribute.CONTROL_NEXTFIRE);
int sortAttr = pc.getSortattribute();
switch (sortAttr) {
case SortAttribute.CONTROL_NEXTFIRE:
schedule = controlScheduleDAO.findByEntityFireTime(id.getType(), id.getID(), pc.isAscending());
break;
case SortAttribute.CONTROL_ACTION:
schedule = controlScheduleDAO.findByEntityAction(id.getType(), id.getID(), pc.isAscending());
break;
default:
throw new NotFoundException("Unknown sort attribute: " + sortAttr);
}
} catch (NotFoundException e) {
throw new ScheduledJobNotFoundException(e);
}
// This will remove stale data and update fire times which
// may result in the list being out of order. We should
// probably sort a second time
PageList<ControlSchedule> list = schedulePager.seek(schedule, pc.getPagenum(), pc.getPagesize());
list.setTotalSize(schedule.size());
return list;
}
/**
* Get a job history based on appdef id
*
*
*
*/
@Transactional(readOnly=true)
public PageList<ControlHistory> findJobHistory(AuthzSubject subject, AppdefEntityID id, PageControl pc)
throws PermissionException, AppdefEntityNotFoundException, GroupNotCompatibleException {
if (id.isGroup()) {
List<AppdefEntityID> groupMembers = GroupUtil
.getCompatGroupMembers(subject, id, null, PageControl.PAGE_ALL);
// For each entity in the list, sanity check permissions
for (AppdefEntityID entity : groupMembers) {
checkControlPermission(subject, entity);
}
} else {
checkControlPermission(subject, id);
}
if (pc == null) {
pc = new PageControl();
}
Collection<ControlHistory> hist;
int sortAttr = pc.getSortattribute();
switch (sortAttr) {
case SortAttribute.CONTROL_ACTION:
hist = controlHistoryDAO.findByEntityAction(id.getType(), id.getID(), pc.isAscending());
break;
case SortAttribute.CONTROL_STATUS:
hist = controlHistoryDAO.findByEntityStatus(id.getType(), id.getID(), pc.isAscending());
break;
case SortAttribute.CONTROL_STARTED:
case SortAttribute.DEFAULT: // default the sorting to the start
hist = controlHistoryDAO.findByEntityStartTime(id.getType(), id.getID(), pc.isAscending());
break;
case SortAttribute.CONTROL_ELAPSED:
hist = controlHistoryDAO.findByEntityDuration(id.getType(), id.getID(), pc.isAscending());
break;
case SortAttribute.CONTROL_DATESCHEDULED:
hist = controlHistoryDAO.findByEntityDateScheduled(id.getType(), id.getID(), pc.isAscending());
break;
case SortAttribute.CONTROL_ENTITYNAME:
// No need to sort since all will have the same name
hist = controlHistoryDAO.findByEntity(id.getType(), id.getID());
break;
default:
throw new SystemException("Unknown sort attribute: " + sortAttr);
}
PageList<ControlHistory> list = historyPager.seek(hist, pc.getPagenum(), pc.getPagesize());
list.setTotalSize(hist.size());
return list;
}
/**
* Get a batch job history based on batchJobId and appdef id
*
*
*
*/
@Transactional(readOnly=true)
public PageList<ControlHistory> findGroupJobHistory(AuthzSubject subject, int batchId, AppdefEntityID id,
PageControl pc) throws ApplicationException {
// default the sorting to the date started
pc = PageControl.initDefaults(pc, SortAttribute.CONTROL_STARTED);
pc.setSortorder(PageControl.SORT_DESC);
Collection<ControlHistory> hist;
int sortAttr = pc.getSortattribute();
switch (sortAttr) {
case SortAttribute.CONTROL_ACTION:
hist = controlHistoryDAO.findByGroupAction(id.getID(), batchId, pc.isAscending());
break;
case SortAttribute.CONTROL_STATUS:
hist = controlHistoryDAO.findByGroupStatus(id.getID(), batchId, pc.isAscending());
break;
case SortAttribute.CONTROL_STARTED:
hist = controlHistoryDAO.findByGroupStartTime(id.getID(), batchId, pc.isAscending());
break;
case SortAttribute.CONTROL_ELAPSED:
hist = controlHistoryDAO.findByGroupDuration(id.getID(), batchId, pc.isAscending());
break;
case SortAttribute.CONTROL_DATESCHEDULED:
hist = controlHistoryDAO.findByGroupDateScheduled(id.getID(), batchId, pc.isAscending());
break;
case SortAttribute.CONTROL_ENTITYNAME:
hist = controlHistoryDAO.findByEntity(id.getType(), id.getID());
break;
default:
throw new ApplicationException("Unknown sort attribute: " + sortAttr);
}
// The the entity names
for (ControlHistory ch : hist) {
AppdefEntityValue aev = new AppdefEntityValue(new AppdefEntityID(ch.getEntityType().intValue(), ch
.getEntityId()), subject);
ch.setEntityName(aev.getName());
}
PageList<ControlHistory> list = historyPager.seek(hist, pc.getPagenum(), pc.getPagesize());
list.setTotalSize(hist.size());
// Sort the list if we are sorting by entity name
if (sortAttr == SortAttribute.CONTROL_ENTITYNAME) {
if (pc.isAscending()) {
Collections.sort(list, new ControlHistoryLocalComparatorAsc());
} else {
Collections.sort(list, new ControlHistoryLocalComparatorDesc());
}
}
return list;
}
/**
* Remove an entry from the control history
*
*
*
*/
@Transactional
public void deleteJobHistory(AuthzSubject subject, Integer[] ids) throws ApplicationException {
// SpringSource: require admin privileges
if (!permissionManager.hasAdminPermission(subject.getId())) {
throw new PermissionException("Admin permission is required to delete job history");
}
// END SpringSource
for (int i = 0; i < ids.length; i++) {
try {
ControlHistory historyLocal = controlHistoryDAO.findById(ids[i]);
controlHistoryDAO.remove(historyLocal);
} catch (ObjectNotFoundException e) {
throw new ApplicationException(e);
}
}
}
/**
* Obtain the current action that is being executed. If there is no current
* running action, null is returned.
*
*
*
*/
@Transactional(readOnly=true)
public ControlHistory getCurrentJob(AuthzSubject whoami, AppdefEntityID id) throws ApplicationException {
Collection<ControlHistory> historyLocals = controlHistoryDAO.findByEntityStartTime(id.getType(), id.getID(),
false);
for (ControlHistory history : historyLocals) {
if (history.getStatus().equals(ControlConstants.STATUS_INPROGRESS)) {
return history;
}
}
return null;
}
/**
* Obtain a control history object based on the history id
*
*
*
*/
@Transactional(readOnly=true)
public ControlHistory getJobByJobId(AuthzSubject subject, Integer id) throws ApplicationException {
try {
return controlHistoryDAO.findById(id);
} catch (ObjectNotFoundException e) {
throw new ApplicationException(e);
}
}
/**
* Obtain the last control action that fired. Returns null if there are no
* previous events. This ignores jobs that are in progress.
*
*
*
*/
@Transactional(readOnly=true)
public ControlHistory getLastJob(AuthzSubject subject, AppdefEntityID id) throws ApplicationException {
Collection<ControlHistory> historyLocals = controlHistoryDAO.findByEntityStartTime(id.getType(), id.getID(),
false);
for (ControlHistory cLocal : historyLocals) {
if (!cLocal.getStatus().equals(ControlConstants.STATUS_INPROGRESS))
return cLocal;
}
return null;
}
/**
* Obtain a scheduled control action based on an id
*
*
*
*/
@Transactional(readOnly=true)
public ControlSchedule getControlJob(AuthzSubject subject, Integer id) throws PluginException {
try {
return controlScheduleDAO.findById(id);
// TODO: validate the job in the scheduler?
} catch (Exception e) {
log.error("Unable to get control job info: " + e.getMessage());
throw new PluginException(e);
}
}
/**
* Delete a scheduled control actions based on id
*
*
*
*/
@Transactional
public void deleteControlJob(AuthzSubject subject, Integer ids[]) throws PluginException {
for (int i = 0; i < ids.length; i++) {
try {
ControlSchedule cScheduleLocal = controlScheduleDAO.findById(ids[i]);
scheduler.deleteJob(cScheduleLocal.getJobName(), GROUP);
controlScheduleDAO.remove(cScheduleLocal);
} catch (Exception e) {
log.error("Unable to remove job: " + e.getMessage());
throw new PluginException(e);
}
}
}
/**
* Removes all jobs associated with an appdef entity
*
*
*/
@Transactional
public void removeScheduledJobs(AuthzSubject subject, AppdefEntityID id) throws ScheduledJobRemoveException {
// Any associated triggers will be automatically removed by Quartz.
Collection<ControlSchedule> jobs = controlScheduleDAO.findByEntity(id.getType(), id.getID());
for (ControlSchedule cSched : jobs) {
try {
scheduler.deleteJob(cSched.getJobName(), GROUP);
} catch (SchedulerException e) {
log.error("Unable to remove job " + cSched.getJobName() + ": " + e.getMessage());
}
controlScheduleDAO.remove(cSched);
}
}
/**
* Schedule an action on an appdef entity
*
*
*
*/
@Transactional
public void scheduleAction(AppdefEntityID id, AuthzSubject subject, String action, ScheduleValue schedule,
int[] order) throws PluginException, SchedulerException {
// Scheduled jobs are persisted in the control subsystem
String jobName = getJobName(subject, id, action);
String triggerName = getTriggerName(subject, id, action);
// Setup the quartz job class that will handle this control action.
Class<?> jobClass = id.isGroup() ? ControlActionGroupJob.class : ControlActionJob.class;
JobDetail jobDetail = new JobDetail(jobName, GROUP, jobClass);
setupJobData(jobDetail, subject, id, action, null, schedule.getScheduleString(), Boolean.TRUE, schedule
.getDescription(), order);
String cronStr;
try {
cronStr = ScheduleParser.getCronString(schedule);
} catch (ScheduleParseException e) {
log.error("Unable to get cron string: " + e.getMessage());
throw new PluginException(e);
}
// Single scheduled actions do not have cron strings
if (cronStr == null) {
SimpleTrigger trigger = new SimpleTrigger(triggerName, GROUP, schedule.getStart());
scheduler.scheduleJob(jobDetail, trigger);
Date nextFire = trigger.getFireTimeAfter(new Date());
if (nextFire == null) {
throw new SchedulerException();
}
try {
controlScheduleDAO.create(id, subject.getName(), action, schedule, nextFire.getTime(), triggerName,
jobName, null);
} catch (Exception e) {
log.error("Unable to schedule job: " + e.getMessage());
throw new PluginException(e);
}
} else {
try {
CronTrigger trigger = new CronTrigger(triggerName, GROUP, jobName, GROUP, schedule.getStart(), schedule
.getEnd(), cronStr);
trigger.setMisfireInstruction(CronTrigger.MISFIRE_INSTRUCTION_DO_NOTHING);
scheduler.scheduleJob(jobDetail, trigger);
// Quartz used to throw an exception on scheduleJob if the
// job would never fire. Guess that is not the case anymore
Date nextFire = trigger.getFireTimeAfter(new Date());
if (nextFire == null) {
throw new ScheduleWillNeverFireException();
}
String stringOrder = null;
if (order != null)
stringOrder = StringUtil.arrayToString(order);
controlScheduleDAO.create(id, subject.getName(), action, schedule, nextFire.getTime(), triggerName,
jobName, stringOrder);
} catch (ParseException e) {
log.error("Unable to setup cron trigger: " + e.getMessage());
throw new PluginException(e);
} catch (SchedulerException e) {
throw new PluginException(e);
} catch (Exception e) {
log.error("Unable to schedule job: " + e.getMessage());
throw new PluginException(e);
}
}
}
private String truncateText(int maxSize, String text) {
String truncatedText = text;
if (text != null) {
if (text.length() > maxSize) {
truncatedText = text.substring(0, maxSize - 3) + "...";
}
}
return truncatedText;
}
/**
* Create a control history entry
*
*
*
*/
@Transactional
public Integer createHistory(AppdefEntityID id, Integer groupId, Integer batchId, String subjectName,
String action, String args, Boolean scheduled, long startTime, long stopTime,
long scheduleTime, String status, String description, String errorMessage) {
return controlHistoryDAO.create(id, groupId, batchId, subjectName, action, truncateText(MAX_HISTORY_TEXT_SIZE,
args), scheduled, startTime, stopTime, scheduleTime, status, truncateText(MAX_HISTORY_TEXT_SIZE,
description), truncateText(MAX_HISTORY_TEXT_SIZE, errorMessage)).getId();
}
/**
* Update a control history entry
*
*
*
*/
@Transactional
public void updateHistory(Integer jobId, long endTime, String status, String message) throws ApplicationException {
ControlHistory local;
try {
local = controlHistoryDAO.findById(jobId);
} catch (ObjectNotFoundException e) {
throw new ApplicationException(e);
}
local.setEndTime(endTime);
local.setStatus(status);
local.setMessage(truncateText(MAX_HISTORY_TEXT_SIZE, message));
}
/**
* Get a control history value based on primary key
*
*
*
*/
@Transactional(readOnly=true)
public ControlHistory getJobHistoryValue(Integer jobId) throws ApplicationException {
try {
return controlHistoryDAO.findByIdAndPopulate(jobId);
} catch (ObjectNotFoundException e) {
throw new ApplicationException(e);
}
}
/**
* Get a control history value based on primary key
*
*
*
*/
@Transactional
public void removeHistory(Integer id) throws ApplicationException {
try {
ControlHistory local = controlHistoryDAO.findById(id);
controlHistoryDAO.remove(local);
} catch (ObjectNotFoundException e) {
throw new ApplicationException(e);
}
}
/**
* Check view permission for an appdef entity
*
* @throws PermissionException if the user does not have the modify
* Operation for the given resource.
*/
private void checkControlPermission(AuthzSubject caller, AppdefEntityID id) throws PermissionException {
permissionManager.checkControlPermission(caller, id);
}
private class ControlHistoryLocalComparatorAsc implements Comparator<ControlHistory> {
public int compare(ControlHistory o1, ControlHistory o2) {
return o1.getEntityName().compareTo(o2.getEntityName());
}
public boolean equals(Object other) {
return false;
}
}
private class ControlHistoryLocalComparatorDesc implements Comparator<ControlHistory> {
public int compare(ControlHistory o1, ControlHistory o2) {
return -(o1.getEntityName().compareTo(o2.getEntityName()));
}
public boolean equals(Object other) {
return false;
}
}
}