package dmg.cells.services.login;
import com.google.common.base.Splitter;
import com.google.common.base.Stopwatch;
import com.google.common.base.Strings;
import com.google.common.net.InetAddresses;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.Callable;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import dmg.cells.nucleus.AbstractCellComponent;
import dmg.cells.nucleus.CellAddressCore;
import dmg.cells.nucleus.CellCommandListener;
import dmg.cells.nucleus.CellEvent;
import dmg.cells.nucleus.CellEventListener;
import dmg.cells.nucleus.CellInfoProvider;
import dmg.cells.nucleus.CellLifeCycleAware;
import dmg.cells.nucleus.CellMessage;
import dmg.cells.nucleus.CellMessageReceiver;
import dmg.cells.nucleus.CellRoute;
import dmg.cells.nucleus.NoRouteToCellException;
import dmg.util.command.Argument;
import dmg.util.command.Command;
import dmg.util.command.Option;
import org.dcache.util.FireAndForgetTask;
import org.dcache.util.NDC;
import org.dcache.util.NetworkUtils;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Strings.isNullOrEmpty;
import static java.util.Arrays.asList;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.MINUTES;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;
/**
* Utility class to periodically publish login broker information.
*/
public class LoginBrokerPublisher
extends AbstractCellComponent
implements CellCommandListener, CellMessageReceiver, CellEventListener, CellLifeCycleAware, CellInfoProvider
{
private static final Logger _log =
LoggerFactory.getLogger(LoginBrokerPublisher.class);
private enum LastEvent
{
NONE, UPDATE_SUBMITTED, UPDATE_SENT, ROUTE_ADDED, NOROUTE
}
private CellAddressCore _topic;
private String _protocolFamily;
private String _protocolVersion;
private String _protocolEngine;
private long _brokerUpdateTime = MINUTES.toMillis(5);
private TimeUnit _brokerUpdateTimeUnit = MILLISECONDS;
private double _brokerUpdateThreshold = 0.1;
private LastEvent _lastEvent = LastEvent.NONE;
private LoadProvider _load = () -> 0.0;
private Supplier<List<InetAddress>> _addresses = createAnyAddressSupplier();
private int _port;
private ScheduledExecutorService _executor;
private ScheduledFuture<?> _task;
private String _root = "/";
private List<String> _readPaths = Collections.emptyList();
private List<String> _writePaths = Collections.emptyList();
private boolean _readEnabled = true;
private boolean _writeEnabled = true;
private List<InetAddress> _lastAddresses = Collections.emptyList();
/**
* Tags to advertise. For thread safety, the list must not be modified.
* Instead it has to be copied and the field must be updated.
*/
private List<String> _tags = Collections.emptyList();
@Command(name = "lb set update", hint = "set login broker update frequency",
description = "Defines how often information about this doors should be published.")
class SetUpdateCommand implements Callable<String>
{
@Argument(metaVar = "seconds")
int time;
@Override
public String call() throws Exception
{
checkArgument(time >= 2, "Update time out of range.");
setBrokerUpdateTime(_brokerUpdateTime, _brokerUpdateTimeUnit);
return "";
}
}
@Command(name = "lb set threshold", hint = "set threshold load for OOB updates",
description = "Sets the relative threshold for sending out-of-band updates. If the " +
"load of this for changes by a factor more than this threshold, an " +
"immediate update is published.")
class SetThresholdCommand implements Callable<String>
{
@Argument
double load;
@Override
public String call() throws Exception
{
setUpdateThreshold(load);
return "";
}
}
@Command(name = "lb set tags", hint = "set published tags",
description = "Doors may be tagged and subscribers of door information may filter " +
"by these tags.")
class SetTagCommand implements Callable<String>
{
@Argument(required = false)
String[] tags;
@Override
public String call() throws Exception
{
setTags(asList(tags));
return "";
}
}
@Command(name = "lb disable", hint = "suspend publishing capabilities",
description = "Allows to temporarily suppress publishing of read and write capabilities. " +
"It will appear as it the door does not authorize access to any read and/or " +
"write paths. Without additional options, both read and write capabilities " +
"will be suspended.\n\n" +
"Note that this does not actually disable the door. Only the advertized " +
"capabilities are changed.")
class DisableCommand implements Callable<String>
{
@Option(name = "read")
boolean read;
@Option(name = "write")
boolean write;
@Override
public String call() throws Exception
{
if (read || !write) {
setReadEnabled(false);
}
if (write || !read) {
setWriteEnabled(false);
}
return "";
}
}
@Command(name = "lb enable", hint = "resume publishing capabilities",
description = "Allows to continue publishing read and/or write capabilities. Without " +
"additional options, both read and write capabilities will be published " +
"in correspondence with the door's configuration.")
class EnableCommand implements Callable<String>
{
@Option(name = "read")
boolean read;
@Option(name = "write")
boolean write;
@Override
public String call() throws Exception
{
if (read || !write) {
setReadEnabled(true);
}
if (write || !read) {
setWriteEnabled(true);
}
return "";
}
}
private synchronized Optional<LoginBrokerInfo> createLoginBrokerInfo(List<InetAddress> addresses)
{
_lastAddresses = addresses;
if (_task != null && !addresses.isEmpty()) {
Collection<String> readPaths = _readEnabled ? _readPaths : Collections.emptyList();
Collection<String> writePaths = _writeEnabled ? _writePaths : Collections.emptyList();
return Optional.of(
new LoginBrokerInfo(getCellName(), getCellDomainName(), _protocolFamily, _protocolVersion,
_protocolEngine, _root, readPaths, writePaths, _tags, addresses, _port,
_load.getLoad(), _brokerUpdateTimeUnit.toMillis(_brokerUpdateTime)));
}
return Optional.empty();
}
private synchronized void sendUpdate(Optional<LoginBrokerInfo> info)
{
if (_topic != null) {
_lastEvent = LastEvent.UPDATE_SENT;
if (info.isPresent()) {
sendMessage(new CellMessage(_topic, info.get()));
}
}
}
protected void submitUpdate()
{
_lastEvent = LastEvent.UPDATE_SUBMITTED;
_executor.execute(this::sendUpdate);
}
private void sendUpdate()
{
sendUpdate(createLoginBrokerInfo(getAddressSupplier().get()));
}
public synchronized void messageArrived(NoRouteToCellException e)
{
CellAddressCore destinationAddress = e.getDestinationPath().getDestinationAddress();
if (_topic != null && destinationAddress.equals(_topic)) {
switch (_lastEvent) {
case UPDATE_SENT:
_lastEvent = LastEvent.NOROUTE;
break;
case ROUTE_ADDED:
submitUpdate();
break;
default:
break;
}
}
}
public LoginBrokerInfo messageArrived(LoginBrokerInfoRequest msg)
{
return createLoginBrokerInfo(getAddressSupplier().get()).orElse(null);
}
@Override
public synchronized void routeAdded(CellEvent ce)
{
CellRoute route = (CellRoute) ce.getSource();
if (route.getRouteType() == CellRoute.TOPIC || route.getRouteType() == CellRoute.DOMAIN) {
switch (_lastEvent) {
case UPDATE_SENT:
_lastEvent = LastEvent.ROUTE_ADDED;
break;
case NOROUTE:
submitUpdate();
break;
default:
break;
}
}
}
@Override
public synchronized void getInfo(PrintWriter pw)
{
if (_topic == null || _task == null) {
pw.println(" Login Broker : DISABLED");
return;
}
pw.println(" LoginBroker : " + _topic);
pw.println(" Protocol Family : " + _protocolFamily);
pw.println(" Protocol Version : " + _protocolVersion);
pw.println(" Port : " + _port);
pw.println(" Addresses : " + _lastAddresses);
pw.println(" Tags : " + _tags);
pw.println(" Root : " + Strings.nullToEmpty(_root));
pw.println(" Read paths : " + _readPaths + (_readEnabled ? "" : " (disabled)"));
pw.println(" Write paths : " + _writePaths + (_writeEnabled ? "" : " (disabled)"));
pw.println(" Update Time : " + _brokerUpdateTime + ' ' + _brokerUpdateTimeUnit);
pw.println(" Update Threshold : " + ((int) (_brokerUpdateThreshold * 100.0)) + " %");
pw.println(" Last event : " + _lastEvent);
}
/**
* Sets the address of the door being published. If null or a wildcard address is provided,
* all interfaces of the door are published. If an address is provided, the canonical
* host is resolved and published with the address. If a name is provided, the name
* is resolved to an address and published together with the name.
*/
public void setAddress(@Nullable String host) throws UnknownHostException
{
if (host == null) {
setAddressSupplier(createAnyAddressSupplier());
} else if (NetworkUtils.isInetAddress(host)) {
InetAddress address = InetAddresses.forString(host);
checkArgument(!address.isMulticastAddress());
if (address.isAnyLocalAddress()) {
setAddressSupplier(createAnyAddressSupplier());
} else {
setAddressSupplier(createSingleAddressSupplier(NetworkUtils.withCanonicalAddress(address)));
}
} else {
setAddressSupplier(createSingleAddressSupplier(InetAddress.getByName(host)));
}
}
/**
* Sets both the port and address from the given socket address.
*/
public synchronized void setSocketAddress(InetSocketAddress socketAddress)
{
InetAddress address = socketAddress.getAddress();
checkArgument(!address.isMulticastAddress());
_port = socketAddress.getPort();
if (address.isAnyLocalAddress()) {
setAddressSupplier(createAnyAddressSupplier());
} else if (NetworkUtils.isInetAddress(socketAddress.getHostString())) {
InetAddress canonicalAddress = NetworkUtils.withCanonicalAddress(address);
setAddressSupplier(() -> Collections.singletonList(canonicalAddress));
} else {
setAddressSupplier(() -> Collections.singletonList(address));
}
}
public synchronized Supplier<List<InetAddress>> getAddressSupplier()
{
return _addresses;
}
public synchronized void setAddressSupplier(Supplier<List<InetAddress>> addresses)
{
_addresses = addresses;
rescheduleTask();
}
public synchronized void setPort(int port)
{
_port = port;
rescheduleTask();
}
public synchronized void setLoad(int children, int maxChildren)
{
double load =
(maxChildren > 0) ? (double) children / (double) maxChildren : 0.0;
setLoadProvider(() -> load);
}
public synchronized void setLoadProvider(LoadProvider load)
{
double diff = Math.abs(_load.getLoad() - load.getLoad());
if (diff > _brokerUpdateThreshold) {
rescheduleTask();
}
_load = load;
}
public synchronized void setTopic(String topic)
{
_topic = new CellAddressCore(topic);
rescheduleTask();
}
public synchronized String getTopic()
{
return Objects.toString(_topic, null);
}
public synchronized void setProtocolFamily(String protocolFamily)
{
_protocolFamily = protocolFamily;
rescheduleTask();
}
public synchronized String getProtocolFamily()
{
return _protocolFamily;
}
public synchronized void setProtocolVersion(String protocolVersion)
{
_protocolVersion = protocolVersion;
rescheduleTask();
}
public synchronized String getProtocolVersion()
{
return _protocolVersion;
}
public synchronized void setProtocolEngine(String protocolEngine)
{
_protocolEngine = protocolEngine;
rescheduleTask();
}
public synchronized String getProtocolEngine()
{
return _protocolEngine;
}
public synchronized void setUpdateThreshold(double threshold)
{
_brokerUpdateThreshold = threshold;
}
public synchronized double getUpdateThreshold()
{
return _brokerUpdateThreshold;
}
public synchronized void setUpdateTime(long time)
{
_brokerUpdateTime = time;
}
public synchronized long getUpdateTime()
{
return _brokerUpdateTime;
}
public synchronized void setUpdateTimeUnit(TimeUnit unit)
{
_brokerUpdateTimeUnit = unit;
rescheduleTask();
}
public synchronized TimeUnit getUpdateTimeUnit()
{
return _brokerUpdateTimeUnit;
}
/**
* Root directory of door.
* <p>
* If null, then a per-user root directory is assumed.
*/
public synchronized void setRoot(String root)
{
_root = root;
}
public synchronized void setReadPaths(List<String> paths)
{
checkArgument(!paths.stream().anyMatch(String::isEmpty));
_readPaths = paths;
rescheduleTask();
}
public synchronized void setWritePaths(List<String> paths)
{
checkArgument(!paths.stream().anyMatch(String::isEmpty));
_writePaths = paths;
rescheduleTask();
}
public synchronized void setTags(List<String> tags)
{
_tags = tags;
rescheduleTask();
}
public synchronized void setWriteEnabled(boolean enabled)
{
_writeEnabled = enabled;
rescheduleTask();
}
public synchronized void setReadEnabled(boolean enabled)
{
_readEnabled = enabled;
rescheduleTask();
}
public synchronized void setBrokerUpdateTime(long time, TimeUnit unit)
{
_brokerUpdateTime = time;
_brokerUpdateTimeUnit = unit;
rescheduleTask();
}
public synchronized void setExecutor(ScheduledExecutorService executor)
{
_executor = executor;
rescheduleTask();
}
@Override
public synchronized void afterStart()
{
scheduleTask();
}
@Override
public synchronized void beforeStop()
{
if (_task != null) {
_task.cancel(true);
_task = null;
}
_addresses = Collections::emptyList;
_writeEnabled = false;
_readEnabled = false;
_load = () -> 1.0;
sendUpdate();
}
@GuardedBy("this")
private void rescheduleTask()
{
if (_task != null) {
_task.cancel(false);
scheduleTask();
}
}
private void scheduleTask()
{
_task = _executor.scheduleWithFixedDelay(
new FireAndForgetTask(this::sendUpdate), 0, _brokerUpdateTime, _brokerUpdateTimeUnit);
}
public static Supplier<List<InetAddress>> createSingleAddressSupplier(InetAddress address)
{
return () -> Collections.singletonList(address);
}
public static Supplier<List<InetAddress>> createAnyAddressSupplier()
{
String localHostAddresses = System.getProperty(NetworkUtils.LOCAL_HOST_ADDRESS_PROPERTY);
if (!isNullOrEmpty(localHostAddresses)) {
List<InetAddress> address = new ArrayList<>();
for (String s : Splitter.on(',').omitEmptyStrings().trimResults().split(localHostAddresses)) {
address.add(NetworkUtils.withCanonicalAddress(InetAddresses.forString(s)));
}
return () -> address;
}
return new AnyAddressSupplier();
}
/**
* Callback interface to query the current load.
*/
public interface LoadProvider
{
double getLoad();
}
public static class AnyAddressSupplier implements Supplier<List<InetAddress>>
{
private List<InetAddress> _previous = Collections.emptyList();
@Override
public List<InetAddress> get()
{
NDC.push("NIC auto-discovery");
try {
ArrayList<InetAddress> addresses = new ArrayList<>();
Stopwatch stopwatch = Stopwatch.createStarted();
try {
Enumeration<NetworkInterface> interfaces =
NetworkInterface.getNetworkInterfaces();
while (interfaces.hasMoreElements()) {
NetworkInterface i = interfaces.nextElement();
try {
if (i.isUp() && !i.isLoopback()) {
Enumeration<InetAddress> e = i.getInetAddresses();
while (e.hasMoreElements()) {
addresses.add(NetworkUtils.withCanonicalAddress(e.nextElement()));
}
}
} catch (SocketException e) {
_log.warn("Not publishing NIC {}: {}", i.getName(), e.getMessage());
}
}
} catch (SocketException e) {
_log.warn("Not publishing NICs: {}", e.getMessage());
}
_log.debug("Scan took {}", stopwatch);
logChanges(addresses);
return addresses;
} finally {
NDC.pop();
}
}
private synchronized void logChanges(List<InetAddress> addresses)
{
if (!_previous.equals(addresses)) {
List<InetAddress> added = addresses.stream().filter(a -> !_previous.contains(a)).collect(toList());
List<InetAddress> removed = _previous.stream().filter(a -> !addresses.contains(a)).collect(toList());
boolean adding = !added.isEmpty();
boolean removing = !removed.isEmpty();
if (removing || adding) {
StringBuilder sb = new StringBuilder();
if (removing) {
sb.append("Removing ").append(describeList(removed));
}
if (adding) {
if (removing) {
sb.append(", adding ");
} else {
sb.append("Adding ");
}
sb.append(describeList(added));
}
_log.warn(sb.toString());
}
_previous = new ArrayList<>(addresses);
}
}
private static String describeList(List<InetAddress> addresses)
{
if (addresses.size() == 1) {
return addresses.get(0).toString();
} else {
return addresses.stream().map(NetworkUtils::toString).collect(joining(", ", "[", "]"));
}
}
}
}