/*******************************************************************************
* This file is part of OpenNMS(R).
*
* Copyright (C) 2008-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.poller.monitors;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.ConnectException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.NoRouteToHostException;
import java.net.PortUnreachableException;
import java.net.Socket;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.util.LinkedHashMap;
import java.util.Map;
import org.apache.log4j.Level;
import org.opennms.core.utils.InetAddressUtils;
import org.opennms.core.utils.ParameterMap;
import org.opennms.core.utils.TimeoutTracker;
import org.opennms.netmgt.model.PollStatus;
import org.opennms.netmgt.poller.Distributable;
import org.opennms.netmgt.poller.MonitoredService;
import org.opennms.netmgt.poller.NetworkInterface;
import org.opennms.netmgt.poller.NetworkInterfaceNotSupportedException;
/**
* This class is designed to be used by the service poller framework to test the
* availability of the trivial UNIX "time" service on remote interfaces. The class
* implements the ServiceMonitor interface that allows it to be used along with
* other plug-ins by the service poller framework.
*
* @author <A HREF="mailto:jeffg@opennms.org">Jeff Gehlbach</A>
* @author <A HREF="http://www.opennms.org/">OpenNMS </A>
*/
@Distributable
final public class TrivialTimeMonitor extends AbstractServiceMonitor {
/**
* Default layer-4 protocol to use
*/
private static final String DEFAULT_PROTOCOL = "tcp"; // Use TCP by default
/**
* Default port.
*/
private static final int DEFAULT_PORT = 37;
/**
* Default retries.
*/
private static final int DEFAULT_RETRY = 0;
/**
* Default timeout. Specifies how long (in milliseconds) to block waiting
* for data from the monitored interface.
*/
private static final int DEFAULT_TIMEOUT = 3000; // 3 second timeout
/**
* Default permissible skew between the remote and local clocks
*/
private static final int DEFAULT_ALLOWED_SKEW = 30; // 30 second skew
/**
* Seconds to subtract from a 1970-01-01 00:00:00-based UNIX timestamp
* to make it comparable to a 1900-01-01 00:00:00-based timestamp from
* the trivial time service (actually adding a negative value)
*/
private static final int EPOCH_ADJ_FACTOR = 2085978496;
/**
* Whether to persist the skew value in addition to the response latency
*/
private static final boolean DEFAULT_PERSIST_SKEW = false;
/**
* {@inheritDoc}
*
* Poll the specified address for service availability.
*
* During the poll an attempt is made to retrieve the time value from the
* remote system. This can be done via either TCP or UDP depending on the
* provided parameters (default TCP). If the time value returned is within
* the specified number of seconds of the local system's clock time, then
* the service is considered available.
*/
public PollStatus poll(MonitoredService svc, Map<String, Object> parameters) {
NetworkInterface<InetAddress> iface = svc.getNetInterface();
//
// Process parameters
//
//
// Get interface address from NetworkInterface
//
if (iface.getType() != NetworkInterface.TYPE_INET)
throw new NetworkInterfaceNotSupportedException("Unsupported interface type, only TYPE_INET currently supported");
TimeoutTracker tracker = new TimeoutTracker(parameters, DEFAULT_RETRY, DEFAULT_TIMEOUT);
// Port
//
int port = ParameterMap.getKeyedInteger(parameters, "port", DEFAULT_PORT);
// Get the address instance.
//
InetAddress ipv4Addr = (InetAddress) iface.getAddress();
if (log().isDebugEnabled())
log().debug("poll: address = " + InetAddressUtils.str(ipv4Addr) + ", port = " + port + ", " + tracker);
// Get the permissible amount of skew.
//
int allowedSkew = ParameterMap.getKeyedInteger(parameters, "allowed-skew", DEFAULT_ALLOWED_SKEW);
// Determine whether to persist the skew value in addition to the latency
boolean persistSkew = ParameterMap.getKeyedBoolean(parameters, "persist-skew", DEFAULT_PERSIST_SKEW);
// Give it a whirl
//
PollStatus serviceStatus = PollStatus.unavailable();
String protocol = ParameterMap.getKeyedString(parameters, "protocol", DEFAULT_PROTOCOL).toLowerCase();
if (! protocol.equalsIgnoreCase("tcp") && ! protocol.equalsIgnoreCase("udp")) {
throw new IllegalArgumentException("Unsupported protocol, only TCP and UDP currently supported");
} else if (protocol.equalsIgnoreCase("udp")) {
// TODO test UDP support
log().warn("UDP support is largely untested");
}
if (protocol.equalsIgnoreCase("tcp")) {
serviceStatus = pollTimeTcp(svc, parameters, serviceStatus, tracker, ipv4Addr, port, allowedSkew, persistSkew);
} else if (protocol.equalsIgnoreCase("udp")) {
serviceStatus = pollTimeUdp(svc, parameters, serviceStatus, tracker, ipv4Addr, port, allowedSkew, persistSkew);
}
//
// return the status of the service
//
return serviceStatus;
}
/**
* <p>storeResult</p>
*
* @param serviceStatus a {@link org.opennms.netmgt.model.PollStatus} object.
* @param skew a {@link java.lang.Number} object.
* @param responseTime a {@link java.lang.Double} object.
* @param persistSkew a boolean.
*/
public void storeResult(PollStatus serviceStatus, Number skew, Double responseTime, boolean persistSkew) {
Map<String,Number> skewProps = new LinkedHashMap<String,Number>();
if (persistSkew) {
skewProps.put("skew", skew);
if (log().isDebugEnabled()) {
log().debug("persistSkew: Persisting time skew (value = " + skew + ") for this node");
}
}
skewProps.put("response-time", responseTime);
serviceStatus.setProperties(skewProps);
}
/**
* <p>pollTimeTcp</p>
*
* @param svc a {@link org.opennms.netmgt.poller.MonitoredService} object.
* @param parameters a {@link java.util.Map} object.
* @param serviceStatus a {@link org.opennms.netmgt.model.PollStatus} object.
* @param tracker a {@link org.opennms.core.utils.TimeoutTracker} object.
* @param ipv4Addr a {@link java.net.InetAddress} object.
* @param port a int.
* @param allowedSkew a int.
* @param persistSkew a boolean.
* @return a {@link org.opennms.netmgt.model.PollStatus} object.
*/
public PollStatus pollTimeTcp(MonitoredService svc, Map<String, Object> parameters, PollStatus serviceStatus, TimeoutTracker tracker, InetAddress ipv4Addr, int port, int allowedSkew, boolean persistSkew) {
int localTime = 0;
int remoteTime = 0;
boolean gotTime = false;
for (tracker.reset(); tracker.shouldRetry() && !gotTime; tracker.nextAttempt()) {
Socket socket = null;
try {
tracker.startAttempt();
socket = new Socket();
socket.connect(new InetSocketAddress(ipv4Addr, port), tracker.getConnectionTimeout());
socket.setSoTimeout(tracker.getSoTimeout());
log().debug("Connected to host: " + ipv4Addr + " on TCP port: " + port);
//
// Try to read from the socket
//
byte[] timeBytes = new byte[4];
ByteBuffer timeByteBuffer = ByteBuffer.wrap(timeBytes);
int bytesRead = socket.getInputStream().read(timeBytes);
if (bytesRead != 4)
continue;
if (log().isDebugEnabled()) {
log().debug("pollTimeTcp: bytes read = " + bytesRead);
}
try {
remoteTime = timeByteBuffer.getInt();
} catch (BufferUnderflowException bue) {
log().error("Encountered buffer underflow while reading time from remote socket.");
remoteTime = 0;
serviceStatus = PollStatus.unavailable("Failed to read a valid time from remote host.");
continue; // to next iteration of for() loop
}
localTime = (int)(System.currentTimeMillis() / 1000) - EPOCH_ADJ_FACTOR;
gotTime = true;
serviceStatus = qualifyTime(remoteTime, localTime, allowedSkew, serviceStatus, tracker.elapsedTimeInMillis(), persistSkew);
} catch (NoRouteToHostException e) {
serviceStatus = logDown(Level.WARN, "No route to host exception for address " + InetAddressUtils.str(ipv4Addr), e);
} catch (InterruptedIOException e) {
serviceStatus = logDown(Level.DEBUG, "did not connect to host with " + tracker);
} catch (ConnectException e) {
serviceStatus = logDown(Level.DEBUG, "Connection exception for address: " + ipv4Addr, e);
} catch (IOException e) {
serviceStatus = logDown(Level.DEBUG, "IOException while polling address: " + ipv4Addr, e);
} finally {
try {
// Close the socket
if (socket != null)
socket.close();
} catch (IOException e) {
e.fillInStackTrace();
if (log().isDebugEnabled())
log().debug("pollTimeTcp: Error closing socket.", e);
}
}
}
return serviceStatus;
}
/**
* <p>pollTimeUdp</p>
*
* @param svc a {@link org.opennms.netmgt.poller.MonitoredService} object.
* @param parameters a {@link java.util.Map} object.
* @param serviceStatus a {@link org.opennms.netmgt.model.PollStatus} object.
* @param tracker a {@link org.opennms.core.utils.TimeoutTracker} object.
* @param ipv4Addr a {@link java.net.InetAddress} object.
* @param port a int.
* @param allowedSkew a int.
* @param persistSkew a boolean.
* @return a {@link org.opennms.netmgt.model.PollStatus} object.
*/
public PollStatus pollTimeUdp(MonitoredService svc, Map<String, Object> parameters, PollStatus serviceStatus, TimeoutTracker tracker, InetAddress ipv4Addr, int port, int allowedSkew, boolean persistSkew) {
int localTime = 0;
int remoteTime = 0;
boolean gotTime = false;
for (tracker.reset(); tracker.shouldRetry() && !gotTime; tracker.nextAttempt()) {
DatagramSocket socket = null;
final String hostAddress = InetAddressUtils.str(ipv4Addr);
try {
tracker.startAttempt();
socket = new DatagramSocket();
socket.setSoTimeout(tracker.getSoTimeout());
log().debug("Requesting time from host: " + ipv4Addr + " on UDP port: " + port);
//
// Send an empty datagram per RFC868
//
socket.send(new DatagramPacket(new byte[]{}, 0, ipv4Addr, port));
//
// Try to receive a response from the remote socket
//
byte[] timeBytes = new byte[4];
ByteBuffer timeByteBuffer = ByteBuffer.wrap(timeBytes);
DatagramPacket timePacket = new DatagramPacket(timeBytes, timeBytes.length, ipv4Addr, port);
socket.receive(timePacket);
int bytesRead = timePacket.getLength();
if (bytesRead != 4)
continue;
if (log().isDebugEnabled()) {
log().debug("pollTimeUdp: bytes read = " + bytesRead);
}
try {
remoteTime = timeByteBuffer.getInt();
} catch (BufferUnderflowException bue) {
log().error("Encountered buffer underflow while reading time from remote socket.");
remoteTime = 0;
serviceStatus = PollStatus.unavailable("Failed to read a valid time from remote host.");
continue; // to next iteration of for() loop
}
localTime = (int)(System.currentTimeMillis() / 1000) - EPOCH_ADJ_FACTOR;
gotTime = true;
serviceStatus = qualifyTime(remoteTime, localTime, allowedSkew, serviceStatus, tracker.elapsedTimeInMillis(), persistSkew);
} catch (PortUnreachableException e) {
serviceStatus = logDown(Level.DEBUG, "Port unreachable exception for address " + hostAddress, e);
} catch (NoRouteToHostException e) {
serviceStatus = logDown(Level.WARN, "No route to host exception for address " + hostAddress, e);
} catch (InterruptedIOException e) {
serviceStatus = logDown(Level.DEBUG, "did not connect to host with " + tracker);
} catch (IOException e) {
serviceStatus = logDown(Level.DEBUG, "IOException while polling address: " + ipv4Addr, e);
} finally {
if (socket != null)
socket.close();
}
}
return serviceStatus;
}
private PollStatus qualifyTime(int remoteTime, int localTime, int allowedSkew, PollStatus serviceStatus, double responseTime, boolean persistSkew) {
if (log().isDebugEnabled()) {
log().debug("qualifyTime: checking remote time " + remoteTime + " against local time " + localTime + " with max skew of " + allowedSkew);
}
if ((localTime - remoteTime > allowedSkew) || (remoteTime - localTime > allowedSkew)) {
serviceStatus = logDown(Level.DEBUG, "Remote time is " + (localTime > remoteTime ? ""+(localTime-remoteTime)+" seconds slow" : ""+(remoteTime-localTime)+" seconds fast"));
}
if ((localTime > remoteTime) && (localTime - remoteTime > allowedSkew)) {
serviceStatus = logDown(Level.DEBUG, "Remote time is " + (localTime - remoteTime) + " seconds behind local, more than the allowable " + allowedSkew);
} else if ((remoteTime > localTime) && (remoteTime - localTime > allowedSkew)) {
serviceStatus = logDown(Level.DEBUG, "Remote time is " + (remoteTime - localTime) + " seconds ahead of local, more than the allowable " + allowedSkew);
} else {
serviceStatus = PollStatus.available();
}
storeResult(serviceStatus, remoteTime - localTime, responseTime, persistSkew);
return serviceStatus;
}
}