/**
* VMware Continuent Tungsten Replicator
* Copyright (C) 2015 VMware, Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Initial developer(s): Robert Hodges
* Contributor(s):
*/
package com.continuent.tungsten.common.network;
import java.net.InetAddress;
import java.net.InterfaceAddress;
import java.net.NetworkInterface;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.apache.log4j.Logger;
import com.continuent.tungsten.common.utils.CLUtils;
/**
* Implements a service for performing operations on Internet addresses, such as
* testing liveness. This class is designed to make calls to ping hosts as
* robust and as simple as possible, at the cost of a little more up-front
* configuration in some cases, for example to set timeouts.
* <p/>
* This class is thread-safe through the use of synchronized methods to access
* the method table and enabled names list. The timeout is volatile, which
* obviates the need for synchronization.
*
* @author <a href="mailto:robert.hodges@continuent.com">Robert Hodges</a>
*/
public class HostAddressService
{
/** Logger for this class */
private static final Logger logger = Logger.getLogger(HostAddressService.class);
/** Default Java ping method using InetAddress.isReachable(). */
public static String DEFAULT = "default";
/** Ping method using operating system ping command. */
public static String PING = "ping";
/** Ping method using the echo server. */
public static String ECHO = "echo";
// Ping methods are stored in a list as well as a hash index. The names list
// contains only enabled methods. Access to these *must* be synchronized to
// preserve thread safety.
private List<String> names = new LinkedList<String>();
private ConcurrentMap<String, String> methods = new ConcurrentHashMap<String, String>();
// Timeout for ping operations in milliseconds.
private volatile int timeoutMillis = 5000;
/**
* Creates a new service.
*
* @param autoEnable If true, enable ping methods automatically.
* @throws HostException Thrown if there is a problem enabling a method
*/
public HostAddressService(boolean autoEnable) throws HostException
{
// Add known ping methods.
addMethod(DEFAULT, InetAddressPing.class.getName(), autoEnable);
addMethod(PING, OsUtilityPing.class.getName(), autoEnable);
addMethod(ECHO, EchoPing.class.getName(), autoEnable);
}
/**
* Sets the timeout for ping methods. Methods will try for up to this time
* before giving up.
*
* @param timeoutMillis Timeout in milliseconds
*/
public void setTimeout(int timeoutMillis)
{
this.timeoutMillis = timeoutMillis;
}
/**
* Returns current timeout in milliseconds.
*/
public int getTimeout()
{
return timeoutMillis;
}
/**
* Adds a ping method to the service.
*
* @param name Logical name of the method
* @param methodClass Method class name
* @param enable If true, enable the method for use
* @throws HostException Thrown if there is a problem enabling a method.
*/
public synchronized void addMethod(String name, String methodClass,
boolean enable) throws HostException
{
if (logger.isDebugEnabled())
{
logger.debug("Adding ping method: name=" + name + " class="
+ methodClass);
}
// Ensure that we can instantiate a ping method. This is a minimal
// check to ensure the ping method will succeed.
instantiatePingMethod(methodClass);
// Add to table of available methods and optionally enable.
methods.put(name, methodClass);
if (enable)
enableMethod(name);
}
/**
* Enables a ping method.
*
* @param name of method to enable
* @throws HostException Thrown if method name does not exist
*/
public synchronized void enableMethod(String name) throws HostException
{
String methodClass = methods.get(name);
if (methodClass == null)
{
StringBuffer sb = new StringBuffer();
for (String legalName : this.getAvailableMethodNames())
{
if (sb.length() > 0)
sb.append(",");
sb.append(legalName);
}
throw new HostException(String.format(
"Unknown ping method name; legal values are (%s): %s",
sb.toString(), name));
}
else
{
if (!names.contains(name))
names.add(name);
}
}
/**
* Returns names of available ping methods, whether enabled or not.
*/
public synchronized List<String> getAvailableMethodNames()
{
Set<String> allNames = this.methods.keySet();
return new ArrayList<String>(allNames);
}
/**
* Returns names of available ping methods.
*/
public synchronized List<String> getEnabledMethodNames()
{
return names;
}
/**
* Returns a ping method by name or null if no such method exists.
*/
public synchronized String getMethodName(String name)
{
return methods.get(name);
}
/** Returns a host address instance. */
public static HostAddress getByName(String host)
throws UnknownHostException
{
InetAddress inetAddress = InetAddress.getByName(host);
HostAddress address = new HostAddress(inetAddress);
return address;
}
/**
* This method returns the host address, in string format, or UNKNOWN if
* there's a problem resolving the address.
*
* @param host
* @return host address in string form or UNKNOWN
*/
public static String getCanonicalAddress(String host)
{
try
{
return (getByName(host).getInetAddress().getHostAddress());
}
catch (Exception e)
{
return "UNKNOWN";
}
}
/**
* Given a pair of addresses and a single network prefix, determines if
* hosts are on the same subnet.
*/
public static boolean addressesAreInSameSubnet(String host1, String host2,
short prefix) throws Exception
{
if (prefix <= 0)
{
throw new Exception("Invalid prefix " + prefix);
}
HostAddress host1Address = getByName(host1);
HostAddress host2Address = getByName(host2);
byte[] netMask = netMaskFromPrefixLength(prefix);
byte[] host1Raw = host1Address.getAddress();
byte[] host2Raw = host2Address.getAddress();
for (int octet = 0; octet < 3; octet++)
{
if ((host1Raw[octet] & netMask[octet]) != (host2Raw[octet] & netMask[octet]))
return false;
}
return true;
}
/**
* This method returns true if the host addresses are equal to each other,
* otherwise false.
*
* @param host1
* @param host2
* @return true if host addresses match, otherwise false.
*/
public static boolean addressesAreEqual(String host1, String host2)
{
try
{
HostAddress host1Address = getByName(host1);
HostAddress host2Address = getByName(host2);
byte[] host1Raw = host1Address.getAddress();
byte[] host2Raw = host2Address.getAddress();
for (int octet = 0; octet < 4; octet++)
{
if (host1Raw[octet] != host2Raw[octet])
return false;
}
return true;
}
catch (Exception e)
{
CLUtils.println(String.format(
"addressesAreEqual(%s, %s) returns FALSE, Exception=%s",
host1, host2, e));
return false;
}
}
/**
* Returns true if the host is reachable by an available ping method. This
* method clears previous notifications.
*
* @param host Name of host for which we want to test reachability
* @return True if host is reachable, otherwise false
* @throws HostException Thrown if a ping method fails
*/
public PingResponse isReachable(HostAddress host) throws HostException
{
// Compose a response.
PingResponse response = new PingResponse();
response.setReachable(false);
// Try all methods.
for (String name : this.getEnabledMethodNames())
{
PingNotification notification = _isReachableByMethod(name, host);
response.addNotification(notification);
response.setReachable(notification.isReachable());
if (response.isReachable())
{
break;
}
}
// Return the response;
return response;
}
/**
* Returns true if the host is reachable by an available ping method. This
* method clears previous notifications.
*
* @param name Name of ping method to use
* @param host Name of host for which we want to test reachability
* @return True if host is reachable, otherwise false
* @throws HostException Thrown if a ping method fails
*/
public PingResponse isReachableByMethod(String name, HostAddress host)
throws HostException
{
PingResponse response = new PingResponse();
PingNotification notification = _isReachableByMethod(name, host);
response.addNotification(notification);
response.setReachable(notification.isReachable());
return response;
}
// Private method to check reachability without clearning notifications.
public PingNotification _isReachableByMethod(String name, HostAddress host)
throws HostException
{
if (logger.isDebugEnabled())
{
logger.debug("Testing host reachability: method=" + name + " host="
+ host.toString() + " timeout=" + timeoutMillis);
}
String methodClass = getMethodName(name);
if (name == null)
{
throw new HostException("Unknown ping method: " + name);
}
else
{
// Set up the notification to be used in the response.
PingNotification notification = new PingNotification();
notification.setHostName(host.getCanonicalHostName());
notification.setMethodName(name);
notification.setTimeout(timeoutMillis);
long startMillis = System.currentTimeMillis();
PingMethod method = null;
try
{
// Instantiate the ping method and prepare it for use.
method = instantiatePingMethod(methodClass);
// Make the call.
boolean status = method.ping(host, timeoutMillis);
// Fill in missing ping information.
notification.setReachable(status);
}
catch (Exception e)
{
// Fill in notification information for an exception.
notification.setReachable(false);
notification.setException(e);
}
finally
{
long duration = System.currentTimeMillis() - startMillis;
notification.setDuration(duration);
notification.setNotes(method.getNotes());
}
// Return the completed notification.
return notification;
}
}
// Instantiates and returns a ping method instance.
private PingMethod instantiatePingMethod(String methodClass)
throws HostException
{
try
{
PingMethod method = (PingMethod) Class.forName(methodClass)
.newInstance();
return method;
}
catch (Throwable e)
{
String msg = String
.format("Unexpected failure while instantiating ping method: name=%s class=%s",
methodClass, methodClass);
throw new HostException(msg, e);
}
}
/**
* This method returns a prefix for a given internet address. It will only
* work on the host for which the address is bound.
*/
public static short getLocalNetworkPrefix(String hostName) throws Exception
{
InetAddress memberAddr = getByName(hostName).getInetAddress();
try
{
Enumeration<NetworkInterface> nets = NetworkInterface
.getNetworkInterfaces();
for (NetworkInterface netint : Collections.list(nets))
{
for (InetAddress inetAddress : Collections.list(netint
.getInetAddresses()))
{
if (inetAddress.equals(memberAddr))
{
List<InterfaceAddress> addresses = netint
.getInterfaceAddresses();
if (addresses == null || addresses.size() == 0)
{
return -1;
}
if (addresses.size() == 1)
{
return addresses.get(0).getNetworkPrefixLength();
}
else
{
return netint.getInterfaceAddresses().get(1)
.getNetworkPrefixLength();
}
}
}
}
}
catch (Exception e)
{
logger.error(String.format(
"Unable to determine network interface for address %s",
memberAddr), e);
}
return -1;
}
public static byte[] netMaskFromPrefixLength(short prefix)
{
if (prefix < 0)
{
return null;
}
int mask = 0xffffffff << (32 - prefix);
int value = mask;
byte[] maskBytes = new byte[]{(byte) (value >>> 24),
(byte) (value >> 16 & 0xff), (byte) (value >> 8 & 0xff),
(byte) (value & 0xff)};
return maskBytes;
}
}