/*******************************************************************************
* This file is part of OpenNMS(R).
*
* Copyright (C) 2010-2011 The OpenNMS Group, Inc.
* OpenNMS(R) is Copyright (C) 1999-2011 The OpenNMS Group, Inc.
*
* OpenNMS(R) is a registered trademark of The OpenNMS Group, Inc.
*
* OpenNMS(R) is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published
* by the Free Software Foundation, either version 3 of the License,
* or (at your option) any later version.
*
* OpenNMS(R) 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 OpenNMS(R). If not, see:
* http://www.gnu.org/licenses/
*
* For more information contact:
* OpenNMS(R) Licensing <license@opennms.org>
* http://www.opennms.org/
* http://www.opennms.com/
*******************************************************************************/
package org.opennms.netmgt.ackd.readers;
import java.io.IOException;
import java.io.LineNumberReader;
import java.io.Reader;
import java.io.StringReader;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.stream.EventFilter;
import javax.xml.stream.XMLEventReader;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.events.StartElement;
import javax.xml.stream.events.XMLEvent;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.HttpRequestInterceptor;
import org.apache.http.HttpResponse;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.AuthState;
import org.apache.http.auth.Credentials;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.protocol.ClientContext;
import org.apache.http.impl.auth.BasicScheme;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.params.CoreConnectionPNames;
import org.apache.http.params.CoreProtocolPNames;
import org.apache.http.protocol.ExecutionContext;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;
import org.hibernate.criterion.Restrictions;
import org.opennms.core.utils.ThreadCategory;
import org.opennms.netmgt.config.ackd.Parameter;
import org.opennms.netmgt.dao.AckdConfigurationDao;
import org.opennms.netmgt.dao.AlarmDao;
import org.opennms.netmgt.model.AckAction;
import org.opennms.netmgt.model.OnmsAcknowledgment;
import org.opennms.netmgt.model.OnmsAlarm;
import org.opennms.netmgt.model.OnmsCriteria;
import org.opennms.netmgt.model.OnmsSeverity;
import org.opennms.netmgt.model.acknowledgments.AckService;
/**
* <p>HypericAckProcessor class.</p>
*
* @author ranger
* @version $Id: $
*/
public class HypericAckProcessor implements AckProcessor {
/** Constant <code>READER_NAME_HYPERIC="HypericReader"</code> */
public static final String READER_NAME_HYPERIC = "HypericReader";
/** Constant <code>PARAMETER_PREFIX_HYPERIC_SOURCE="source:"</code> */
public static final String PARAMETER_PREFIX_HYPERIC_SOURCE = "source:";
/** Constant <code>ALERTS_PER_HTTP_TRANSACTION=200</code> */
public static final int ALERTS_PER_HTTP_TRANSACTION = 200;
// public static final String PARAMETER_HYPERIC_HOSTS = "hyperic-hosts";
private AckdConfigurationDao m_ackdDao;
private AlarmDao m_alarmDao;
private AckService m_ackService;
/**
* <p>This class is used as the data bean for parsing XML responses from the Hyperic HQ
* systems that are serving up our alert status groovy servlet. The expected data
* format is:</p>
* <pre>
* <?xml version="1.0" encoding="UTF-8"?>
* <hyperic-alert-statuses>
* <alert id="1" ack="true" fixed="true"/>
* <alert id="2" ack="true" fixed="true"/>
* <alert id="3" ack="true" fixed="false"/>
* <alert id="4" ack="false" fixed="true"/>
* <alert id="5" ack="false" fixed="false"/>
* </hyperic-alert-statuses>
* </pre>
*/
@XmlRootElement(name="hyperic-alert-statuses")
static class HypericAlertStatuses {
private List<HypericAlertStatus> statusList;
@XmlElement
public List<HypericAlertStatus> getStatusList() {
return statusList;
}
public void setStatusList(List<HypericAlertStatus> statusList) {
this.statusList = statusList;
}
}
/**
* <p>This class represents each individual alarm status within the message. The expected
* format is:</p>
* <pre>
* <alert id="1" ack="true" fixed="true"/>
* </pre>
*
* <p>TODO: Add ackUser, ackTime, fixedUser, fixedTime attributes to objects if possible</p>
*/
@XmlRootElement(name="alert")
static class HypericAlertStatus {
private int alertId;
private String ackUser;
private String ackMessage;
private Date ackTime;
private boolean isFixed;
private String fixUser;
private String fixMessage;
private Date fixTime;
@XmlAttribute(name="id", required=true)
public int getAlertId() {
return alertId;
}
public void setAlertId(int alertId) {
this.alertId = alertId;
}
@XmlAttribute(name="fixed", required=true)
public boolean isFixed() {
return isFixed;
}
public void setFixed(boolean isFixed) {
this.isFixed = isFixed;
}
@XmlAttribute(name="ackUser")
public String getAckUser() {
return ackUser;
}
public void setAckUser(String ackUser) {
this.ackUser = ackUser;
}
@XmlAttribute(name="ackMessage")
public String getAckMessage() {
return ackMessage;
}
public void setAckMessage(String ackMessage) {
this.ackMessage = ackMessage;
}
@XmlAttribute(name="ackTime")
public Date getAckTime() {
return ackTime;
}
public void setAckTime(Date ackTime) {
this.ackTime = ackTime;
}
@XmlAttribute(name="fixUser")
public String getFixUser() {
return fixUser;
}
public void setFixUser(String fixUser) {
this.fixUser = fixUser;
}
@XmlAttribute(name="fixMessage")
public String getFixMessage() {
return fixMessage;
}
public void setFixMessage(String fixMessage) {
this.fixMessage = fixMessage;
}
@XmlAttribute(name="fixTime")
public Date getFixTime() {
return fixTime;
}
public void setFixTime(Date fixTime) {
this.fixTime = fixTime;
}
public String toString() {
StringBuffer retval = new StringBuffer();
retval.append("{ ");
retval.append("id: ").append(String.valueOf(alertId)).append(", ");
retval.append("fixed: ").append(String.valueOf(isFixed)).append(", ");
retval.append("ackUser: ").append(String.valueOf(ackUser)).append(", ");
retval.append("ackMessage: ").append(String.valueOf(ackMessage)).append(", ");
retval.append("ackTime: ").append(String.valueOf(ackTime)).append(", ");
retval.append("fixUser: ").append(String.valueOf(fixUser)).append(", ");
retval.append("fixMessage: ").append(String.valueOf(fixMessage)).append(", ");
retval.append("fixTime: ").append(String.valueOf(fixTime));
retval.append(" }");
return retval.toString();
}
}
private static ThreadCategory log() {
return ThreadCategory.getInstance(HypericAckProcessor.class);
}
/// TODO Verify that this works properly
/**
* <p>reloadConfigs</p>
*/
public void reloadConfigs() {
log().debug("reloadConfigs: reloading configuration...");
m_ackdDao.reloadConfiguration();
log().debug("reloadConfigs: configuration reloaded");
}
/**
* <p>fetchUnclearedHypericAlarms</p>
*
* @return a {@link java.util.List} object.
*/
public List<OnmsAlarm> fetchUnclearedHypericAlarms() {
// Query for existing, unacknowledged alarms in OpenNMS that were generated based on Hyperic alerts
OnmsCriteria criteria = new OnmsCriteria(OnmsAlarm.class, "alarm");
// criteria.add(Restrictions.isNull("alarmAckUser"));
// Restrict to Hyperic alerts
criteria.add(Restrictions.eq("uei", "uei.opennms.org/external/hyperic/alert"));
// Only consider alarms that are above severity NORMAL
// {@see org.opennms.netmgt.model.OnmsSeverity}
criteria.add(Restrictions.gt("severity", OnmsSeverity.NORMAL));
// TODO Figure out how to query by parameters (maybe necessary)
// Query list of outstanding alerts with remote platform identifiers
return m_alarmDao.findMatching(criteria);
}
/**
* <p>getUrlForHypericSource</p>
*
* @param source a {@link java.lang.String} object.
* @return a {@link java.lang.String} object.
*/
public String getUrlForHypericSource(String source) {
if (source == null) {
throw new IllegalArgumentException("Cannot search for null Hyperic platform IDs inside the ackd configuration");
} else if ("".equals(source)) {
throw new IllegalArgumentException("Cannot search for blank Hyperic platform IDs inside the ackd configuration");
}
List<Parameter> params = m_ackdDao.getParametersForReader(READER_NAME_HYPERIC);
if (params == null) {
throw new IllegalStateException("There is no configuration for the '" + READER_NAME_HYPERIC + "' reader inside the ackd configuration");
}
for (Parameter param : params) {
if ((PARAMETER_PREFIX_HYPERIC_SOURCE + source).equalsIgnoreCase(param.getKey())) {
return param.getValue();
}
}
return null;
}
/**
* <p>run</p>
*/
public void run() {
List<OnmsAcknowledgment> acks = new ArrayList<OnmsAcknowledgment>();
try {
log().info("run: Processing Hyperic acknowledgments..." );
// Query list of outstanding alerts with remote platform identifiers
List<OnmsAlarm> unAckdAlarms = fetchUnclearedHypericAlarms();
Map<String,List<OnmsAlarm>> organizedAlarms = new TreeMap<String,List<OnmsAlarm>>();
int legacyAlarmCount = 0;
// Split the list of alarms up according to the Hyperic system where they originated
for (OnmsAlarm alarm : unAckdAlarms) {
String key = getAlertSourceParmValue(alarm);
if (key == null || "".equals(key)) {
legacyAlarmCount++;
} else {
List<OnmsAlarm> targetList = organizedAlarms.get(key);
if (targetList == null) {
targetList = new ArrayList<OnmsAlarm>();
organizedAlarms.put(key, targetList);
}
targetList.add(alarm);
}
}
if (legacyAlarmCount > 0) {
log().info(String.valueOf(legacyAlarmCount) + " Hyperic alarms without an alert.source param found, these alarms will not be processed");
}
// Connect to each Hyperic system and query for the status of corresponding alerts
for (Map.Entry<String, List<OnmsAlarm>> alarmList : organizedAlarms.entrySet()) {
String hypericSystem = alarmList.getKey();
List<OnmsAlarm> alarmsForSystem = alarmList.getValue();
// Match the alert.source to the Hyperic URL via the config
String hypericUrl = getUrlForHypericSource(hypericSystem);
if (hypericUrl == null) {
// If the alert.source doesn't match anything in our current config, just ignore it, warn in the logs
log().warn("Could not find Hyperic host URL for the following platform ID: " + hypericSystem);
log().warn("Skipping processing of " + alarmsForSystem.size() + " alarms with that platform ID");
continue;
}
try {
List<String> alertIdList = new ArrayList<String>();
for (OnmsAlarm alarmForSystem : alarmList.getValue()) {
// Construct a sane query for the Hyperic system
String alertId = getAlertIdParmValue(alarmForSystem);
alertIdList.add(alertId);
}
// Call fetchHypericAlerts() for each system
List<HypericAlertStatus> alertsForSystem = fetchHypericAlerts(hypericUrl, alertIdList);
// Iterate and update any acknowledged or fixed alerts
for (HypericAlertStatus alert : alertsForSystem) {
OnmsAlarm alarm = findAlarmForHypericAlert(alarmsForSystem, hypericSystem, alert);
if (alarm == null) {
log().warn("Could not find the OpenNMS alarm for the following Hyperic alert: URL: \"" + hypericUrl + "\", id: " + alert.getAlertId());
} else if (alert.isFixed() && !OnmsSeverity.CLEARED.equals(alarm.getSeverity())) {
// If the Hyperic alert has been fixed and the local alarm is not yet marked as CLEARED, then clear it
OnmsAcknowledgment ack = new OnmsAcknowledgment(alarm, "Ackd.HypericAckProcessor", (alert.getFixTime() != null) ? alert.getFixTime() : new Date());
ack.setAckAction(AckAction.CLEAR);
ack.setLog(alert.getFixMessage());
acks.add(ack);
} else if(alert.getAckMessage() != null && alarm.getAckTime() == null) {
// If the Hyperic alert has been ack'd and the local alarm is not yet ack'd, then ack it
OnmsAcknowledgment ack = new OnmsAcknowledgment(alarm, "Ackd.HypericAckProcessor", (alert.getAckTime() != null) ? alert.getAckTime() : new Date());
ack.setAckAction(AckAction.ACKNOWLEDGE);
ack.setLog(alert.getAckMessage());
acks.add(ack);
}
}
} catch (Throwable e) {
log().warn("run: threw exception when processing alarms for Hyperic system " + hypericSystem + ": " + e.getMessage());
log().warn("run: " + acks.size() + " acknowledgements processed successfully before exception");
} finally {
if (acks.size() > 0) {
m_ackService.processAcks(acks);
}
}
}
log().info("run: Finished processing Hyperic acknowledgments (" + acks.size() + " ack(s) processed for " + unAckdAlarms.size() + " alarm(s))" );
} catch (Throwable e) {
log().warn("run: threw exception: " + e.getMessage(), e);
}
}
/**
* <p>findAlarmForHypericAlert</p>
*
* @param alarms a {@link java.util.List} object.
* @param platformId a {@link java.lang.String} object.
* @param alert a {@link org.opennms.netmgt.ackd.readers.HypericAckProcessor.HypericAlertStatus} object.
* @return a {@link org.opennms.netmgt.model.OnmsAlarm} object.
*/
public static OnmsAlarm findAlarmForHypericAlert(List<OnmsAlarm> alarms, String platformId, HypericAlertStatus alert) {
String targetPlatformId = "alert.source=" + platformId + "(string,text)";
String targetAlertId = "alert.id="+ String.valueOf(alert.getAlertId()) + "(string,text)";
for (OnmsAlarm alarm : alarms) {
String parmString = alarm.getEventParms();
String[] parms = parmString.split(";");
for (String parm : parms) {
if (targetPlatformId.equals(parm)) {
for (String alertparm : parms) {
if (targetAlertId.equals(alertparm)) {
return alarm;
}
}
}
}
}
return null;
}
/**
* <p>getAlertSourceParmValue</p>
*
* @param alarm a {@link org.opennms.netmgt.model.OnmsAlarm} object.
* @return a {@link java.lang.String} object.
*/
public static String getAlertSourceParmValue(OnmsAlarm alarm) {
return getParmValueByRegex(alarm, "alert.source=(.*)[(]string,text[)]");
}
/**
* <p>getAlertIdParmValue</p>
*
* @param alarm a {@link org.opennms.netmgt.model.OnmsAlarm} object.
* @return a {@link java.lang.String} object.
*/
public static String getAlertIdParmValue(OnmsAlarm alarm) {
return getParmValueByRegex(alarm, "alert.id=([0-9]*)[(]string,text[)]");
}
/**
* <p>Some parameter values that you might be interested in inside this class:</p>
*
* <ul>
* <li><code>alert.id</code>: ID of the alert in the remote Hyperic HQ system</li>
* <li><code>alert.baseURL</code>: Base URL of the Hyperic HQ service that generated the alert</li>
* <li><code>alert.source</code>: String key that identifies the Hyperic HQ service that generated the alert</li>
* </ul>
*
* @param alarm The alarm to fetch parameters from
* @param regex Java regex expression with a () group that will be returned
* @return The matching group from the regex
*/
public static String getParmValueByRegex(OnmsAlarm alarm, String regex) {
Pattern pattern = Pattern.compile(regex);
String parmString = alarm.getEventParms();
String[] parms = parmString.split(";");
for (String parm : parms) {
Matcher matcher = pattern.matcher(parm);
if (matcher.matches()) {
return matcher.group(1);
}
}
return null;
}
/**
* <p>fetchHypericAlerts</p>
*
* @param hypericUrl a {@link java.lang.String} object.
* @param alertIds a {@link java.util.List} object.
* @return a {@link java.util.List} object.
* @throws org.apache.commons.httpclient.HttpException if any.
* @throws java.io.IOException if any.
* @throws javax.xml.bind.JAXBException if any.
* @throws javax.xml.stream.XMLStreamException if any.
*/
public static List<HypericAlertStatus> fetchHypericAlerts(String hypericUrl, List<String> alertIds) throws IOException, JAXBException, XMLStreamException {
List<HypericAlertStatus> retval = new ArrayList<HypericAlertStatus>();
if (alertIds.size() < 1) {
return retval;
}
for (int i = 0; i < alertIds.size(); i++) {
// Construct the query string for the HTTP operation
StringBuffer alertIdString = new StringBuffer();
alertIdString.append("?");
for (int j = 0; (j < ALERTS_PER_HTTP_TRANSACTION) && (i < alertIds.size()); j++,i++) {
if (j > 0) alertIdString.append("&");
// Numeric values, no need to worry about URL encoding
alertIdString.append("id=").append(alertIds.get(i));
}
// Create an HTTP client
DefaultHttpClient httpClient = new DefaultHttpClient();
httpClient.getParams().setIntParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, 3000);
httpClient.getParams().setIntParameter(CoreConnectionPNames.SO_TIMEOUT, 3000);
HttpUriRequest httpMethod = new HttpGet(hypericUrl + alertIdString.toString());
// Set a custom user-agent so that it's easy to tcpdump these requests
httpMethod.getParams().setParameter(CoreProtocolPNames.USER_AGENT, "OpenNMS-Ackd.HypericAckProcessor");
// Parse the URI from the config so that we can deduce the username/password information
String userinfo = null;
try {
URI hypericUri = new URI(hypericUrl);
userinfo = hypericUri.getUserInfo();
// httpMethod.getParams().setParameter(ClientPNames.VIRTUAL_HOST, new HttpHost("localhost", hypericUri.getPort()));
} catch (URISyntaxException e) {
log().warn("Could not parse URI to get username/password stanza: " + hypericUrl, e);
}
if (userinfo != null && !"".equals(userinfo)) {
// Add the credentials to the HttpClient instance
httpClient.getCredentialsProvider().setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(userinfo));
/**
* Add an HttpRequestInterceptor that will perform preemptive auth
* @see http://hc.apache.org/httpcomponents-client-4.0.1/tutorial/html/authentication.html
*/
HttpRequestInterceptor preemptiveAuth = new HttpRequestInterceptor() {
public void process(final HttpRequest request, final HttpContext context) throws IOException {
AuthState authState = (AuthState)context.getAttribute(ClientContext.TARGET_AUTH_STATE);
CredentialsProvider credsProvider = (CredentialsProvider)context.getAttribute(ClientContext.CREDS_PROVIDER);
HttpHost targetHost = (HttpHost)context.getAttribute(ExecutionContext.HTTP_TARGET_HOST);
// If not auth scheme has been initialized yet
if (authState.getAuthScheme() == null) {
AuthScope authScope = new AuthScope(targetHost.getHostName(), targetHost.getPort());
// Obtain credentials matching the target host
Credentials creds = credsProvider.getCredentials(authScope);
// If found, generate BasicScheme preemptively
if (creds != null) {
authState.setAuthScheme(new BasicScheme());
authState.setCredentials(creds);
}
}
}
};
httpClient.addRequestInterceptor(preemptiveAuth, 0);
}
try {
HttpResponse response = httpClient.execute(httpMethod);
// int statusCode = response.getStatusLine().getStatusCode();
// String statusText = response.getStatusLine().getReasonPhrase();
retval = parseHypericAlerts(new StringReader(EntityUtils.toString(response.getEntity())));
} finally{
// Do we need to do any cleanup?
// httpMethod.releaseConnection();
}
}
return retval;
}
/**
* <p>parseHypericAlerts</p>
*
* @param reader a {@link java.io.Reader} object.
* @return a {@link java.util.List} object.
* @throws javax.xml.bind.JAXBException if any.
* @throws javax.xml.stream.XMLStreamException if any.
*/
public static List<HypericAlertStatus> parseHypericAlerts(Reader reader) throws JAXBException, XMLStreamException {
List<HypericAlertStatus> retval = new ArrayList<HypericAlertStatus>();
// Instantiate a JAXB context to parse the alert status
JAXBContext context = JAXBContext.newInstance(new Class[] { HypericAlertStatuses.class, HypericAlertStatus.class });
XMLInputFactory xmlif = XMLInputFactory.newInstance();
XMLEventReader xmler = xmlif.createXMLEventReader(reader);
EventFilter filter = new EventFilter() {
public boolean accept(XMLEvent event) {
return event.isStartElement();
}
};
XMLEventReader xmlfer = xmlif.createFilteredReader(xmler, filter);
// Read up until the beginning of the root element
StartElement startElement = (StartElement)xmlfer.nextEvent();
// Fetch the root element name for {@link HypericAlertStatus} objects
String rootElementName = context.createJAXBIntrospector().getElementName(new HypericAlertStatuses()).getLocalPart();
if (rootElementName.equals(startElement.getName().getLocalPart())) {
Unmarshaller unmarshaller = context.createUnmarshaller();
// Use StAX to pull parse the incoming alert statuses
while (xmlfer.peek() != null) {
Object object = unmarshaller.unmarshal(xmler);
if (object instanceof HypericAlertStatus) {
HypericAlertStatus alertStatus = (HypericAlertStatus)object;
retval.add(alertStatus);
}
}
} else {
// Try to pull in the HTTP response to give the user a better idea of what went wrong
StringBuffer errorContent = new StringBuffer();
LineNumberReader lineReader = new LineNumberReader(reader);
try {
String line;
while (true) {
line = lineReader.readLine();
if (line == null) {
break;
} else {
errorContent.append(line.trim());
}
}
} catch (IOException e) {
errorContent.append("Exception while trying to print out message content: " + e.getMessage());
}
// Throw an exception and include the erroneous HTTP response in the exception text
throw new JAXBException("Found wrong root element in Hyperic XML document, expected: \"" + rootElementName + "\", found \"" + startElement.getName().getLocalPart() + "\"\n" +
errorContent.toString());
}
return retval;
}
/**
* <p>setAckdConfigDao</p>
*
* @param configDao a {@link org.opennms.netmgt.dao.AckdConfigurationDao} object.
*/
public synchronized void setAckdConfigDao(final AckdConfigurationDao configDao) {
m_ackdDao = configDao;
}
/**
* <p>setAckService</p>
*
* @param ackService a {@link org.opennms.netmgt.model.acknowledgments.AckService} object.
*/
public synchronized void setAckService(final AckService ackService) {
m_ackService = ackService;
}
/**
* <p>afterPropertiesSet</p>
*
* @throws java.lang.Exception if any.
*/
@Override
public void afterPropertiesSet() throws Exception {
}
/**
* <p>setAlarmDao</p>
*
* @param dao a {@link org.opennms.netmgt.dao.AlarmDao} object.
*/
public synchronized void setAlarmDao(final AlarmDao dao) {
m_alarmDao = dao;
}
}