/* * JBoss, Home of Professional Open Source. * Copyright 2012, Red Hat, Inc., and individual contributors * as indicated by the @author tags. See the copyright.txt file in the * distribution for a full listing of individual contributors. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.jboss.as.server.mgmt.domain; import javax.security.auth.callback.Callback; import javax.security.auth.callback.CallbackHandler; import javax.security.auth.callback.NameCallback; import javax.security.auth.callback.PasswordCallback; import javax.security.auth.callback.UnsupportedCallbackException; import javax.security.sasl.RealmCallback; import javax.security.sasl.RealmChoiceCallback; import java.io.DataInput; import java.io.IOException; import java.io.InterruptedIOException; import java.net.URI; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import org.jboss.as.controller.ModelController; import org.jboss.as.controller.remote.ResponseAttachmentInputStreamSupport; import org.jboss.as.controller.remote.TransactionalProtocolClient; import org.jboss.as.controller.remote.TransactionalProtocolOperationHandler; import org.jboss.as.protocol.ProtocolConnectionConfiguration; import org.jboss.as.protocol.ProtocolConnectionManager; import org.jboss.as.protocol.ProtocolConnectionUtils; import org.jboss.as.protocol.StreamUtils; import org.jboss.as.protocol.mgmt.AbstractManagementRequest; import org.jboss.as.protocol.mgmt.ActiveOperation; import org.jboss.as.protocol.mgmt.FlushableDataOutput; import org.jboss.as.protocol.mgmt.FutureManagementChannel; import org.jboss.as.protocol.mgmt.ManagementChannelHandler; import org.jboss.as.protocol.mgmt.ManagementPingRequest; import org.jboss.as.protocol.mgmt.ManagementRequestContext; import org.jboss.as.remoting.management.ManagementRemotingServices; import org.jboss.as.server.logging.ServerLogger; import org.jboss.dmr.ModelNode; import org.jboss.remoting3.Channel; import org.jboss.remoting3.Connection; import org.wildfly.security.manager.WildFlySecurityManager; /** * The connection to the host-controller. In case the channel is closed it's the host-controllers responsibility * to ask individual managed servers to reconnect. * * @author Emanuel Muckenhuber */ class HostControllerConnection extends FutureManagementChannel { private static final String SERVER_CHANNEL_TYPE = ManagementRemotingServices.SERVER_CHANNEL; private static final long reconnectionDelay; static { // Since there is the remoting connection timeout we might not need a delay between reconnection attempts at all reconnectionDelay = Long.parseLong(WildFlySecurityManager.getPropertyPrivileged("jboss.as.domain.host.reconnection.delay", "1500")); } private final String userName; private final String serverProcessName; private final ProtocolConnectionManager connectionManager; private final ManagementChannelHandler channelHandler; private final ExecutorService executorService; private final int initialOperationID; private final ResponseAttachmentInputStreamSupport responseAttachmentSupport; private volatile ProtocolConnectionConfiguration configuration; private volatile ReconnectRunner reconnectRunner; HostControllerConnection(final String serverProcessName, final String userName, final int initialOperationID, final ProtocolConnectionConfiguration configuration, final ResponseAttachmentInputStreamSupport responseAttachmentSupport, final ExecutorService executorService) { this.userName = userName; this.serverProcessName = serverProcessName; this.configuration = configuration; this.initialOperationID = initialOperationID; this.executorService = executorService; this.channelHandler = new ManagementChannelHandler(this, executorService); this.connectionManager = ProtocolConnectionManager.create(configuration, this, new ReconnectTask()); this.responseAttachmentSupport = responseAttachmentSupport; } ManagementChannelHandler getChannelHandler() { return channelHandler; } @Override public Channel getChannel() throws IOException { final Channel channel = super.getChannel(); if(channel == null) { // Fail fast, don't try to await a new channel throw channelClosed(); } return channel; } /** * Connect to the HC and retrieve the current model updates. * * @param controller the server controller * @param callback the operation completed callback * * @throws IOException for any error */ synchronized void openConnection(final ModelController controller, final ActiveOperation.CompletedCallback<ModelNode> callback) throws Exception { boolean ok = false; final Connection connection = connectionManager.connect(); try { channelHandler.executeRequest(new ServerRegisterRequest(), null, callback); // HC is the same version, so it will support sending the subject channelHandler.getAttachments().attach(TransactionalProtocolClient.SEND_SUBJECT, Boolean.TRUE); channelHandler.addHandlerFactory(new TransactionalProtocolOperationHandler(controller, channelHandler, responseAttachmentSupport)); ok = true; } finally { if(!ok) { connection.close(); } } } /** * This continuously tries to reconnect in a separate thread and will only stop if the connection was established * successfully or the server gets shutdown. If there is currently a reconnect task active the connection paramaters * and callback will get updated. * * @param reconnectUri the updated connection uri * @param authKey the updated authentication key * @param callback the current callback */ synchronized void asyncReconnect(final URI reconnectUri, String authKey, final ReconnectCallback callback) { if (getState() != State.OPEN) { return; } // Update the configuration with the new credentials final ProtocolConnectionConfiguration config = ProtocolConnectionConfiguration.copy(configuration); config.setCallbackHandler(createClientCallbackHandler(userName, authKey)); config.setUri(reconnectUri); this.configuration = config; final ReconnectRunner reconnectTask = this.reconnectRunner; if (reconnectTask == null) { final ReconnectRunner task = new ReconnectRunner(); task.callback = callback; task.future = executorService.submit(task); } else { reconnectTask.callback = callback; } } /** * Reconnect to the HC. * * @return whether the server is still in sync * @throws IOException */ synchronized boolean doReConnect() throws IOException { // In case we are still connected, test the connection and see if we can reuse it if(connectionManager.isConnected()) { try { final Future<Long> result = channelHandler.executeRequest(ManagementPingRequest.INSTANCE, null).getResult(); result.get(15, TimeUnit.SECONDS); // Hmm, perhaps 15 is already too much return true; } catch (Exception e) { ServerLogger.AS_ROOT_LOGGER.debugf(e, "failed to ping the host-controller, going to reconnect"); } // Disconnect - the HC might have closed the connection without us noticing and is asking for a reconnect final Connection connection = connectionManager.getConnection(); StreamUtils.safeClose(connection); if(connection != null) { try { // Wait for the connection to be closed connection.awaitClosed(); } catch (InterruptedException e) { throw new InterruptedIOException(); } } } boolean ok = false; final Connection connection = connectionManager.connect(); try { // Reconnect to the host-controller final ActiveOperation<Boolean, Void> result = channelHandler.executeRequest(new ServerReconnectRequest(), null); try { boolean inSync = result.getResult().get(); ok = true; reconnectRunner = null; return inSync; } catch (ExecutionException e) { throw new IOException(e); } catch (InterruptedException e) { throw new InterruptedIOException(); } } finally { if(!ok) { StreamUtils.safeClose(connection); } } } /** * Send the started notification */ synchronized void started() { try { if(isConnected()) { channelHandler.executeRequest(new ServerStartedRequest(), null).getResult().await(); } } catch (Exception e) { ServerLogger.AS_ROOT_LOGGER.debugf(e, "failed to send started notification"); } } @Override public void connectionOpened(final Connection connection) throws IOException { final Channel channel = openChannel(connection, SERVER_CHANNEL_TYPE, configuration.getOptionMap()); if(setChannel(channel)) { channel.receiveMessage(channelHandler.getReceiver()); channel.addCloseHandler(channelHandler); } else { channel.closeAsync(); } } @Override public void close() throws IOException { try { super.close(); final ReconnectRunner reconnectTask = this.reconnectRunner; if (reconnectTask != null) { this.reconnectRunner = null; reconnectTask.cancel(); } } finally { connectionManager.shutdown(); } } /** * The server registration request. */ private class ServerRegisterRequest extends AbstractManagementRequest<ModelNode, Void> { @Override public byte getOperationType() { return DomainServerProtocol.REGISTER_REQUEST; } @Override protected void sendRequest(final ActiveOperation.ResultHandler<ModelNode> resultHandler, final ManagementRequestContext<Void> context, final FlushableDataOutput output) throws IOException { output.writeUTF(serverProcessName); output.writeInt(initialOperationID); } @Override public void handleRequest(DataInput input, ActiveOperation.ResultHandler<ModelNode> resultHandler, ManagementRequestContext<Void> voidManagementRequestContext) throws IOException { final byte param = input.readByte(); if(param == DomainServerProtocol.PARAM_OK) { final ModelNode operations = new ModelNode(); operations.readExternal(input); resultHandler.done(operations); } else { resultHandler.failed(new IOException()); } } } /** * The server reconnect request. Additionally to registering the server at the HC, the response will * contain whether this server is still in sync or needs to be restarted. */ public class ServerReconnectRequest extends AbstractManagementRequest<Boolean, Void> { @Override public byte getOperationType() { return DomainServerProtocol.SERVER_RECONNECT_REQUEST; } @Override protected void sendRequest(final ActiveOperation.ResultHandler<Boolean> resultHandler, final ManagementRequestContext<Void> context, final FlushableDataOutput output) throws IOException { output.write(DomainServerProtocol.PARAM_SERVER_NAME); output.writeUTF(serverProcessName); } @Override public void handleRequest(final DataInput input, final ActiveOperation.ResultHandler<Boolean> resultHandler, final ManagementRequestContext<Void> context) throws IOException { final byte param = input.readByte(); context.executeAsync(new ManagementRequestContext.AsyncTask<Void>() { @Override public void execute(ManagementRequestContext<Void> voidManagementRequestContext) throws Exception { if(param == DomainServerProtocol.PARAM_OK) { // Still in sync with the HC resultHandler.done(Boolean.TRUE); } else { // Out of sync, set restart-required resultHandler.done(Boolean.FALSE); } } }, false); } } public class ServerStartedRequest extends AbstractManagementRequest<Void, Void> { private final String message = ""; // started / failed message @Override public byte getOperationType() { return DomainServerProtocol.SERVER_STARTED_REQUEST; } @Override protected void sendRequest(ActiveOperation.ResultHandler<Void> resultHandler, ManagementRequestContext<Void> voidManagementRequestContext, FlushableDataOutput output) throws IOException { output.write(DomainServerProtocol.PARAM_OK); // TODO handle server start failed message output.writeUTF(message); resultHandler.done(null); } @Override public void handleRequest(DataInput input, ActiveOperation.ResultHandler<Void> resultHandler, ManagementRequestContext<Void> voidManagementRequestContext) throws IOException { // } } private class ReconnectTask implements ProtocolConnectionManager.ConnectTask { @Override public Connection connect() throws IOException { // Reconnect with a potentially new configuration return ProtocolConnectionUtils.connectSync(configuration); } @Override public ProtocolConnectionManager.ConnectionOpenHandler getConnectionOpenedHandler() { return HostControllerConnection.this; } @Override public ProtocolConnectionManager.ConnectTask connectionClosed() { ServerLogger.AS_ROOT_LOGGER.debugf("Connection to Host Controller closed"); return this; } @Override public void shutdown() { // } } /** * Create the client callback handler. * * @param userName the username * @param authKey the authentication key * @return the callback handler */ static CallbackHandler createClientCallbackHandler(final String userName, final String authKey) { return new ClientCallbackHandler(userName, authKey); } private static class ClientCallbackHandler implements CallbackHandler { private final String userName; private final String authKey; private ClientCallbackHandler(String userName, String authKey) { this.userName = userName; this.authKey = authKey; } public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { for (Callback current : callbacks) { if (current instanceof RealmCallback) { RealmCallback rcb = (RealmCallback) current; String defaultText = rcb.getDefaultText(); rcb.setText(defaultText); // For now just use the realm suggested. } else if (current instanceof RealmChoiceCallback) { throw new UnsupportedCallbackException(current, "Realm choice not currently supported."); } else if (current instanceof NameCallback) { NameCallback ncb = (NameCallback) current; ncb.setName(userName); } else if (current instanceof PasswordCallback) { PasswordCallback pcb = (PasswordCallback) current; pcb.setPassword(authKey.toCharArray()); } else { throw new UnsupportedCallbackException(current); } } } } interface ReconnectCallback { /** * Callback on reconnection. * * @param inSync whether the server is still in sync with the host-controller */ void reconnected(final boolean inSync); } class ReconnectRunner implements Runnable { private volatile Future<?> future; private volatile ReconnectCallback callback; @Override public synchronized void run() { final boolean outcome; try { outcome = doReConnect(); callback.reconnected(outcome); reconnectRunner = null; } catch (Exception e) { try { Thread.sleep(reconnectionDelay); } catch (InterruptedException i) { Thread.currentThread().interrupt(); } if (getState() == State.OPEN) { ServerLogger.AS_ROOT_LOGGER.failedToConnectToHostController(); future = executorService.submit(this); } } } public void cancel() { if (future != null) { future.cancel(true); } } } }