/*
* Copyright 2016 the original author or authors.
*
* 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.
*/
package org.springframework.data.hadoop.util.net;
import java.net.InetAddress;
import java.net.InterfaceAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.List;
import org.apache.commons.net.util.SubnetUtils;
import org.apache.commons.net.util.SubnetUtils.SubnetInfo;
import org.springframework.data.hadoop.util.net.HostInfoDiscovery;
import org.springframework.util.StringUtils;
/**
* Default implementation of {@link HostInfoDiscovery}.
* <p>
* Discovery logic for finding ip address is:
* <ul>
* <li>all possible network interfaces are requested
* <li>for interfaces, filter out point to point if not enabled
* <li>for interfaces, filter out loopback if not enabled
* <li>for interfaces, explicit regex patter match is done if pattern is set
* <li>interfaces are sort by preferred name prefixes, on default "eth" and "en" are sort first
* <li>interfaces are sorted by their indexes assuming eth0 should be picked over eth1
* <li>interfaces are checked by their list of ip addresses
* <li>only ipv4 ip's are taken
* <li>cidr notation to match if from a network/mask is taken in defined
* <li>what is left, first found ip is taken
* </ul>
*
* @author Janne Valkealahti
*
*/
public class DefaultHostInfoDiscovery implements HostInfoDiscovery {
private String matchIpv4;
private String matchInterface;
private List<String> preferInterface = Arrays.asList("eth", "en");
private boolean pointToPoint = false;
private boolean loopback = false;
@Override
public HostInfo getHostInfo() {
List<NetworkInterface> interfaces;
try {
interfaces = getAllAvailableInterfaces();
} catch (SocketException e) {
return null;
}
// pre filter candidates
interfaces = filterInterfaces(interfaces);
// sort to prepare getting first match
interfaces = sortInterfaces(interfaces);
for (NetworkInterface nic : interfaces) {
List<InetAddress> addresses = new ArrayList<InetAddress>();
for (InterfaceAddress interfaceAddress : nic.getInterfaceAddresses()) {
addresses.add(interfaceAddress.getAddress());
}
addresses = filterAddresses(addresses);
if (!addresses.isEmpty()) {
InetAddress address = addresses.get(0);
return new HostInfo(address.getHostAddress(), address.getHostName());
}
}
return null;
}
/**
* Sets the match ipv4. Used to match ip address from
* a network using a cidr notation. For example, "192.168.0.1/24"
* matches range "192.168.0.1-192.168.0.254", "192.168.0.1/16"
* matches ranre "192.168.0.1-192.168.255.254" and
* "10.0.0.1/8" matches range "10.0.0.1-10.255.255.254"
*
* @param matchIpv4 the new match ipv4
*/
public void setMatchIpv4(String matchIpv4) {
this.matchIpv4 = matchIpv4;
}
/**
* Use interface as a candidate if its name is matching with a
* given pattern. Default value is is empty.
*
* @param matchInterface the new match interface regex patter
* @see NetworkInterface#getName()
*/
public void setMatchInterface(String matchInterface) {
this.matchInterface = matchInterface;
}
/**
* Sets the preferred interfaces. Sort interfaces in such
* order that interface names prefixed with values found from
* a list is considered first candidates. Defaults to "eth" and
* "en" which usually are the ones found from unix systems.
*
* @param preferInterface the new preferred interface list
*/
public void setPreferInterface(List<String> preferInterface) {
this.preferInterface = preferInterface;
}
/**
* Sets if interfaces marked as point to point should be handled.
* Point to point nic is usually a vpn tunnel which may not be no
* use to talk to a host unless communication goes through vpn.
* Default value is <code>FALSE</code>.
*
* @param pointToPoint the new point to point flag
* @see NetworkInterface#isPointToPoint()
*/
public void setPointToPoint(boolean pointToPoint) {
this.pointToPoint = pointToPoint;
}
/**
* Sets if loopback should be discovered.
* Default value is <code>FALSE</code>.
*
* @param loopback the new loopback flag
*/
public void setLoopback(boolean loopback) {
this.loopback = loopback;
}
protected List<NetworkInterface> getAllAvailableInterfaces() throws SocketException {
List<NetworkInterface> interfaces = new ArrayList<NetworkInterface>(5);
for (Enumeration<NetworkInterface> e = NetworkInterface.getNetworkInterfaces(); e.hasMoreElements();) {
interfaces.add(e.nextElement());
}
return interfaces;
}
private List<InetAddress> filterAddresses(List<InetAddress> addresses) {
List<InetAddress> filtered = new ArrayList<InetAddress>();
for (InetAddress address : addresses) {
// take only ipv4 addresses whose byte array length is always 4
boolean match = address.getAddress() != null && address.getAddress().length == 4;
// check loopback
if (!loopback && address.isLoopbackAddress()) {
match = false;
}
// match cidr if defined
if (match && StringUtils.hasText(matchIpv4)) {
match = matchIpv4(matchIpv4, address.getHostAddress());
}
if (match) {
filtered.add(address);
}
}
return filtered;
}
private boolean matchIpv4(String addressMatch, String address) {
// TODO: should we fork utils from jakarta commons?
SubnetUtils subnetUtils = new SubnetUtils(addressMatch);
SubnetInfo info = subnetUtils.getInfo();
return info.isInRange(address);
}
private List<NetworkInterface> filterInterfaces(List<NetworkInterface> interfaces) {
List<NetworkInterface> filtered = new ArrayList<NetworkInterface>();
for (NetworkInterface nic : interfaces) {
boolean match = false;
try {
match = pointToPoint && nic.isPointToPoint();
} catch (SocketException e) {
}
try {
match = !match && loopback && nic.isLoopback();
} catch (SocketException e) {
}
// last, if we didn't match anything, let all pass
// if matchInterface is not set, otherwise do pattern
// matching
if (!match && !StringUtils.hasText(matchInterface)) {
match = true;
} else if (StringUtils.hasText(matchInterface)) {
match = nic.getName().matches(matchInterface);
}
if (match) {
filtered.add(nic);
}
}
return filtered;
}
private List<NetworkInterface> sortInterfaces(List<NetworkInterface> interfaces) {
Collections.sort(interfaces, new NicIndexComparator());
Collections.sort(interfaces, new NicPreferNameComparator());
return interfaces;
}
/**
* Comparator to sort with nic index.
*/
private class NicIndexComparator implements Comparator<NetworkInterface> {
@Override
public int compare(NetworkInterface o1, NetworkInterface o2) {
return Integer.compare(o1.getIndex(), o2.getIndex());
}
}
/**
* Comparator for nic names preferring a list of give prefixes order
* to sort those before any other.
*/
private class NicPreferNameComparator implements Comparator<NetworkInterface> {
@Override
public int compare(NetworkInterface o1, NetworkInterface o2) {
String o1name = o1.getName();
String o2name = o2.getName();
if (startWithAny(preferInterface, o1name) && startWithAny(preferInterface, o2name)) {
return 0;
} else if (startWithAny(preferInterface, o1name) && !startWithAny(preferInterface, o2name)) {
return -1;
} else if (!startWithAny(preferInterface, o1name) && startWithAny(preferInterface, o2name)) {
return 1;
} else {
return o1name.compareTo(o2name);
}
}
private boolean startWithAny(List<String> prefixes, String name) {
for (String prefix : prefixes) {
if (name.startsWith(prefix)) {
return true;
}
}
return false;
}
}
@Override
public String toString() {
return "DefaultHostInfoDiscovery [matchIpv4=" + matchIpv4 + ", matchInterface=" + matchInterface + ", preferInterface="
+ preferInterface + ", pointToPoint=" + pointToPoint + ", loopback=" + loopback + "]";
}
}