http://git-wip-us.apache.org/repos/asf/cloudstack/blob/83fd8f60/utils/src/main/java/com/cloud/utils/nio/NioClient.java ---------------------------------------------------------------------- diff --git a/utils/src/main/java/com/cloud/utils/nio/NioClient.java b/utils/src/main/java/com/cloud/utils/nio/NioClient.java new file mode 100644 index 0000000..2f742f9 --- /dev/null +++ b/utils/src/main/java/com/cloud/utils/nio/NioClient.java @@ -0,0 +1,125 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 com.cloud.utils.nio; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.SocketChannel; +import java.security.GeneralSecurityException; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; + +import org.apache.log4j.Logger; + +import org.apache.cloudstack.utils.security.SSLUtils; + +public class NioClient extends NioConnection { + private static final Logger s_logger = Logger.getLogger(NioClient.class); + + protected String _host; + protected String _bindAddress; + protected SocketChannel _clientConnection; + + public NioClient(String name, String host, int port, int workers, HandlerFactory factory) { + super(name, port, workers, factory); + _host = host; + } + + public void setBindAddress(String ipAddress) { + _bindAddress = ipAddress; + } + + @Override + protected void init() throws IOException { + _selector = Selector.open(); + Task task = null; + + try { + _clientConnection = SocketChannel.open(); + _clientConnection.configureBlocking(true); + s_logger.info("Connecting to " + _host + ":" + _port); + + if (_bindAddress != null) { + s_logger.info("Binding outbound interface at " + _bindAddress); + + InetSocketAddress bindAddr = new InetSocketAddress(_bindAddress, 0); + _clientConnection.socket().bind(bindAddr); + } + + InetSocketAddress peerAddr = new InetSocketAddress(_host, _port); + _clientConnection.connect(peerAddr); + + SSLEngine sslEngine = null; + // Begin SSL handshake in BLOCKING mode + _clientConnection.configureBlocking(true); + + SSLContext sslContext = Link.initSSLContext(true); + sslEngine = sslContext.createSSLEngine(_host, _port); + sslEngine.setUseClientMode(true); + sslEngine.setEnabledProtocols(SSLUtils.getSupportedProtocols(sslEngine.getEnabledProtocols())); + + Link.doHandshake(_clientConnection, sslEngine, true); + s_logger.info("SSL: Handshake done"); + s_logger.info("Connected to " + _host + ":" + _port); + + _clientConnection.configureBlocking(false); + Link link = new Link(peerAddr, this); + link.setSSLEngine(sslEngine); + SelectionKey key = _clientConnection.register(_selector, SelectionKey.OP_READ); + link.setKey(key); + key.attach(link); + // Notice we've already connected due to the handshake, so let's get the + // remaining task done + task = _factory.create(Task.Type.CONNECT, link, null); + } catch (GeneralSecurityException e) { + _selector.close(); + throw new IOException("Failed to initialise security", e); + } catch (IOException e) { + _selector.close(); + throw e; + } + + _executor.execute(task); + } + + @Override + protected void registerLink(InetSocketAddress saddr, Link link) { + // don't do anything. + } + + @Override + protected void unregisterLink(InetSocketAddress saddr) { + // don't do anything. + } + + @Override + public void cleanUp() throws IOException { + super.cleanUp(); + if (_clientConnection != null) { + _clientConnection.close(); + } + s_logger.info("NioClient connection closed"); + + } + +}
http://git-wip-us.apache.org/repos/asf/cloudstack/blob/83fd8f60/utils/src/main/java/com/cloud/utils/nio/NioConnection.java ---------------------------------------------------------------------- diff --git a/utils/src/main/java/com/cloud/utils/nio/NioConnection.java b/utils/src/main/java/com/cloud/utils/nio/NioConnection.java new file mode 100644 index 0000000..4c66360 --- /dev/null +++ b/utils/src/main/java/com/cloud/utils/nio/NioConnection.java @@ -0,0 +1,476 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 com.cloud.utils.nio; + +import static com.cloud.utils.AutoCloseableUtil.closeAutoCloseable; + +import java.io.IOException; +import java.net.ConnectException; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.nio.channels.CancelledKeyException; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.ServerSocketChannel; +import java.nio.channels.SocketChannel; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; + +import org.apache.log4j.Logger; + +import org.apache.cloudstack.utils.security.SSLUtils; + +import com.cloud.utils.concurrency.NamedThreadFactory; + +/** + * NioConnection abstracts the NIO socket operations. The Java implementation + * provides that. + */ +public abstract class NioConnection implements Runnable { + private static final Logger s_logger = Logger.getLogger(NioConnection.class);; + + protected Selector _selector; + protected Thread _thread; + protected boolean _isRunning; + protected boolean _isStartup; + protected int _port; + protected List<ChangeRequest> _todos; + protected HandlerFactory _factory; + protected String _name; + protected ExecutorService _executor; + + public NioConnection(String name, int port, int workers, HandlerFactory factory) { + _name = name; + _isRunning = false; + _thread = null; + _selector = null; + _port = port; + _factory = factory; + _executor = new ThreadPoolExecutor(workers, 5 * workers, 1, TimeUnit.DAYS, new LinkedBlockingQueue<Runnable>(), new NamedThreadFactory(name + "-Handler")); + } + + public void start() { + _todos = new ArrayList<ChangeRequest>(); + + _thread = new Thread(this, _name + "-Selector"); + _isRunning = true; + _thread.start(); + // Wait until we got init() done + synchronized (_thread) { + try { + _thread.wait(); + } catch (InterruptedException e) { + s_logger.warn("Interrupted start thread ", e); + } + } + } + + public void stop() { + _executor.shutdown(); + _isRunning = false; + if (_thread != null) { + _thread.interrupt(); + } + } + + public boolean isRunning() { + return _thread.isAlive(); + } + + public boolean isStartup() { + return _isStartup; + } + + @Override + public void run() { + synchronized (_thread) { + try { + init(); + } catch (ConnectException e) { + s_logger.warn("Unable to connect to remote: is there a server running on port " + _port); + return; + } catch (IOException e) { + s_logger.error("Unable to initialize the threads.", e); + return; + } catch (Exception e) { + s_logger.error("Unable to initialize the threads due to unknown exception.", e); + return; + } + _isStartup = true; + _thread.notifyAll(); + } + + while (_isRunning) { + try { + _selector.select(); + + // Someone is ready for I/O, get the ready keys + Set<SelectionKey> readyKeys = _selector.selectedKeys(); + Iterator<SelectionKey> i = readyKeys.iterator(); + + if (s_logger.isTraceEnabled()) { + s_logger.trace("Keys Processing: " + readyKeys.size()); + } + // Walk through the ready keys collection. + while (i.hasNext()) { + SelectionKey sk = i.next(); + i.remove(); + + if (!sk.isValid()) { + if (s_logger.isTraceEnabled()) { + s_logger.trace("Selection Key is invalid: " + sk.toString()); + } + Link link = (Link)sk.attachment(); + if (link != null) { + link.terminated(); + } else { + closeConnection(sk); + } + } else if (sk.isReadable()) { + read(sk); + } else if (sk.isWritable()) { + write(sk); + } else if (sk.isAcceptable()) { + accept(sk); + } else if (sk.isConnectable()) { + connect(sk); + } + } + + s_logger.trace("Keys Done Processing."); + + processTodos(); + } catch (Throwable e) { + s_logger.warn("Caught an exception but continuing on.", e); + } + } + synchronized (_thread) { + _isStartup = false; + } + } + + abstract void init() throws IOException; + + abstract void registerLink(InetSocketAddress saddr, Link link); + + abstract void unregisterLink(InetSocketAddress saddr); + + protected void accept(SelectionKey key) throws IOException { + ServerSocketChannel serverSocketChannel = (ServerSocketChannel)key.channel(); + + SocketChannel socketChannel = serverSocketChannel.accept(); + Socket socket = socketChannel.socket(); + socket.setKeepAlive(true); + + if (s_logger.isTraceEnabled()) { + s_logger.trace("Connection accepted for " + socket); + } + + // Begin SSL handshake in BLOCKING mode + socketChannel.configureBlocking(true); + + SSLEngine sslEngine = null; + try { + SSLContext sslContext = Link.initSSLContext(false); + sslEngine = sslContext.createSSLEngine(); + sslEngine.setUseClientMode(false); + sslEngine.setNeedClientAuth(false); + sslEngine.setEnabledProtocols(SSLUtils.getSupportedProtocols(sslEngine.getEnabledProtocols())); + + Link.doHandshake(socketChannel, sslEngine, false); + + } catch (Exception e) { + if (s_logger.isTraceEnabled()) { + s_logger.trace("Socket " + socket + " closed on read. Probably -1 returned: " + e.getMessage()); + } + closeAutoCloseable(socketChannel, "accepting socketChannel"); + closeAutoCloseable(socket, "opened socket"); + return; + } + + if (s_logger.isTraceEnabled()) { + s_logger.trace("SSL: Handshake done"); + } + socketChannel.configureBlocking(false); + InetSocketAddress saddr = (InetSocketAddress)socket.getRemoteSocketAddress(); + Link link = new Link(saddr, this); + link.setSSLEngine(sslEngine); + link.setKey(socketChannel.register(key.selector(), SelectionKey.OP_READ, link)); + Task task = _factory.create(Task.Type.CONNECT, link, null); + registerLink(saddr, link); + _executor.execute(task); + } + + protected void terminate(SelectionKey key) { + Link link = (Link)key.attachment(); + closeConnection(key); + if (link != null) { + link.terminated(); + Task task = _factory.create(Task.Type.DISCONNECT, link, null); + unregisterLink(link.getSocketAddress()); + _executor.execute(task); + } + } + + protected void read(SelectionKey key) throws IOException { + Link link = (Link)key.attachment(); + try { + SocketChannel socketChannel = (SocketChannel)key.channel(); + if (s_logger.isTraceEnabled()) { + s_logger.trace("Reading from: " + socketChannel.socket().toString()); + } + byte[] data = link.read(socketChannel); + if (data == null) { + if (s_logger.isTraceEnabled()) { + s_logger.trace("Packet is incomplete. Waiting for more."); + } + return; + } + Task task = _factory.create(Task.Type.DATA, link, data); + _executor.execute(task); + } catch (Exception e) { + logDebug(e, key, 1); + terminate(key); + } + } + + protected void logTrace(Exception e, SelectionKey key, int loc) { + if (s_logger.isTraceEnabled()) { + Socket socket = null; + if (key != null) { + SocketChannel ch = (SocketChannel)key.channel(); + if (ch != null) { + socket = ch.socket(); + } + } + + s_logger.trace("Location " + loc + ": Socket " + socket + " closed on read. Probably -1 returned."); + } + } + + protected void logDebug(Exception e, SelectionKey key, int loc) { + if (s_logger.isDebugEnabled()) { + Socket socket = null; + if (key != null) { + SocketChannel ch = (SocketChannel)key.channel(); + if (ch != null) { + socket = ch.socket(); + } + } + + s_logger.debug("Location " + loc + ": Socket " + socket + " closed on read. Probably -1 returned: " + e.getMessage()); + } + } + + protected void processTodos() { + List<ChangeRequest> todos; + if (_todos.size() == 0) { + return; // Nothing to do. + } + + synchronized (this) { + todos = _todos; + _todos = new ArrayList<ChangeRequest>(); + } + + if (s_logger.isTraceEnabled()) { + s_logger.trace("Todos Processing: " + todos.size()); + } + SelectionKey key; + for (ChangeRequest todo : todos) { + switch (todo.type) { + case ChangeRequest.CHANGEOPS: + try { + key = (SelectionKey)todo.key; + if (key != null && key.isValid()) { + if (todo.att != null) { + key.attach(todo.att); + Link link = (Link)todo.att; + link.setKey(key); + } + key.interestOps(todo.ops); + } + } catch (CancelledKeyException e) { + s_logger.debug("key has been cancelled"); + } + break; + case ChangeRequest.REGISTER: + try { + key = ((SocketChannel)(todo.key)).register(_selector, todo.ops, todo.att); + if (todo.att != null) { + Link link = (Link)todo.att; + link.setKey(key); + } + } catch (ClosedChannelException e) { + s_logger.warn("Couldn't register socket: " + todo.key); + try { + ((SocketChannel)(todo.key)).close(); + } catch (IOException ignore) { + s_logger.info("[ignored] socket channel"); + } finally { + Link link = (Link)todo.att; + link.terminated(); + } + } + break; + case ChangeRequest.CLOSE: + if (s_logger.isTraceEnabled()) { + s_logger.trace("Trying to close " + todo.key); + } + key = (SelectionKey)todo.key; + closeConnection(key); + if (key != null) { + Link link = (Link)key.attachment(); + if (link != null) { + link.terminated(); + } + } + break; + default: + s_logger.warn("Shouldn't be here"); + throw new RuntimeException("Shouldn't be here"); + } + } + s_logger.trace("Todos Done processing"); + } + + protected void connect(SelectionKey key) throws IOException { + SocketChannel socketChannel = (SocketChannel)key.channel(); + + try { + socketChannel.finishConnect(); + key.interestOps(SelectionKey.OP_READ); + Socket socket = socketChannel.socket(); + if (!socket.getKeepAlive()) { + socket.setKeepAlive(true); + } + if (s_logger.isDebugEnabled()) { + s_logger.debug("Connected to " + socket); + } + Link link = new Link((InetSocketAddress)socket.getRemoteSocketAddress(), this); + link.setKey(key); + key.attach(link); + Task task = _factory.create(Task.Type.CONNECT, link, null); + _executor.execute(task); + } catch (IOException e) { + logTrace(e, key, 2); + terminate(key); + } + } + + protected void scheduleTask(Task task) { + _executor.execute(task); + } + + protected void write(SelectionKey key) throws IOException { + Link link = (Link)key.attachment(); + try { + if (s_logger.isTraceEnabled()) { + s_logger.trace("Writing to " + link.getSocketAddress().toString()); + } + boolean close = link.write((SocketChannel)key.channel()); + if (close) { + closeConnection(key); + link.terminated(); + } else { + key.interestOps(SelectionKey.OP_READ); + } + } catch (Exception e) { + logDebug(e, key, 3); + terminate(key); + } + } + + protected void closeConnection(SelectionKey key) { + if (key != null) { + SocketChannel channel = (SocketChannel)key.channel(); + key.cancel(); + try { + if (channel != null) { + if (s_logger.isDebugEnabled()) { + s_logger.debug("Closing socket " + channel.socket()); + } + channel.close(); + } + } catch (IOException ignore) { + s_logger.info("[ignored] channel"); + } + } + } + + public void register(int ops, SocketChannel key, Object att) { + ChangeRequest todo = new ChangeRequest(key, ChangeRequest.REGISTER, ops, att); + synchronized (this) { + _todos.add(todo); + } + _selector.wakeup(); + } + + public void change(int ops, SelectionKey key, Object att) { + ChangeRequest todo = new ChangeRequest(key, ChangeRequest.CHANGEOPS, ops, att); + synchronized (this) { + _todos.add(todo); + } + _selector.wakeup(); + } + + public void close(SelectionKey key) { + ChangeRequest todo = new ChangeRequest(key, ChangeRequest.CLOSE, 0, null); + synchronized (this) { + _todos.add(todo); + } + _selector.wakeup(); + } + + /* Release the resource used by the instance */ + public void cleanUp() throws IOException { + if (_selector != null) { + _selector.close(); + } + } + + public class ChangeRequest { + public static final int REGISTER = 1; + public static final int CHANGEOPS = 2; + public static final int CLOSE = 3; + + public Object key; + public int type; + public int ops; + public Object att; + + public ChangeRequest(Object key, int type, int ops, Object att) { + this.key = key; + this.type = type; + this.ops = ops; + this.att = att; + } + } +} http://git-wip-us.apache.org/repos/asf/cloudstack/blob/83fd8f60/utils/src/main/java/com/cloud/utils/nio/NioServer.java ---------------------------------------------------------------------- diff --git a/utils/src/main/java/com/cloud/utils/nio/NioServer.java b/utils/src/main/java/com/cloud/utils/nio/NioServer.java new file mode 100644 index 0000000..98a4a51 --- /dev/null +++ b/utils/src/main/java/com/cloud/utils/nio/NioServer.java @@ -0,0 +1,97 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 com.cloud.utils.nio; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.SelectionKey; +import java.nio.channels.ServerSocketChannel; +import java.nio.channels.spi.SelectorProvider; +import java.util.WeakHashMap; + +import org.apache.log4j.Logger; + +public class NioServer extends NioConnection { + private final static Logger s_logger = Logger.getLogger(NioServer.class); + + protected InetSocketAddress _localAddr; + private ServerSocketChannel _serverSocket; + + protected WeakHashMap<InetSocketAddress, Link> _links; + + public NioServer(String name, int port, int workers, HandlerFactory factory) { + super(name, port, workers, factory); + _localAddr = null; + _links = new WeakHashMap<InetSocketAddress, Link>(1024); + } + + @Override + protected void init() throws IOException { + _selector = SelectorProvider.provider().openSelector(); + + _serverSocket = ServerSocketChannel.open(); + _serverSocket.configureBlocking(false); + + _localAddr = new InetSocketAddress(_port); + _serverSocket.socket().bind(_localAddr); + + _serverSocket.register(_selector, SelectionKey.OP_ACCEPT, null); + + s_logger.info("NioConnection started and listening on " + _localAddr.toString()); + } + + @Override + public void cleanUp() throws IOException { + super.cleanUp(); + if (_serverSocket != null) { + _serverSocket.close(); + } + s_logger.info("NioConnection stopped on " + _localAddr.toString()); + } + + @Override + protected void registerLink(InetSocketAddress addr, Link link) { + _links.put(addr, link); + } + + @Override + protected void unregisterLink(InetSocketAddress saddr) { + _links.remove(saddr); + } + + /** + * Sends the data to the address specified. If address is not already + * connected, this does nothing and returns null. If address is already + * connected, then it returns the attached object so the caller can + * prepare for any responses. + * @param saddr + * @param data + * @return null if not sent. attach object in link if sent. + */ + public Object send(InetSocketAddress saddr, byte[] data) throws ClosedChannelException { + Link link = _links.get(saddr); + if (link == null) { + return null; + } + link.send(data); + return link.attachment(); + } +} http://git-wip-us.apache.org/repos/asf/cloudstack/blob/83fd8f60/utils/src/main/java/com/cloud/utils/nio/Task.java ---------------------------------------------------------------------- diff --git a/utils/src/main/java/com/cloud/utils/nio/Task.java b/utils/src/main/java/com/cloud/utils/nio/Task.java new file mode 100644 index 0000000..c77c703 --- /dev/null +++ b/utils/src/main/java/com/cloud/utils/nio/Task.java @@ -0,0 +1,89 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 com.cloud.utils.nio; + +import org.apache.log4j.Logger; + +/** + * Task represents one todo item for the AgentManager or the AgentManager + * + */ +public abstract class Task implements Runnable { + private static final Logger s_logger = Logger.getLogger(Task.class); + + public enum Type { + CONNECT, // Process a new connection. + DISCONNECT, // Process an existing connection disconnecting. + DATA, // data incoming. + CONNECT_FAILED, // Connection failed. + OTHER // Allows other tasks to be defined by the caller. + }; + + Object _data; + Type _type; + Link _link; + + public Task(Type type, Link link, byte[] data) { + _data = data; + _type = type; + _link = link; + } + + public Task(Type type, Link link, Object data) { + _data = data; + _type = type; + _link = link; + } + + protected Task() { + } + + public Type getType() { + return _type; + } + + public Link getLink() { + return _link; + } + + public byte[] getData() { + return (byte[])_data; + } + + public Object get() { + return _data; + } + + @Override + public String toString() { + return _type.toString(); + } + + abstract protected void doTask(Task task) throws Exception; + + @Override + public final void run() { + try { + doTask(this); + } catch (Throwable e) { + s_logger.warn("Caught the following exception but pushing on", e); + } + } +} http://git-wip-us.apache.org/repos/asf/cloudstack/blob/83fd8f60/utils/src/main/java/com/cloud/utils/nio/TrustAllManager.java ---------------------------------------------------------------------- diff --git a/utils/src/main/java/com/cloud/utils/nio/TrustAllManager.java b/utils/src/main/java/com/cloud/utils/nio/TrustAllManager.java new file mode 100644 index 0000000..5dffd62 --- /dev/null +++ b/utils/src/main/java/com/cloud/utils/nio/TrustAllManager.java @@ -0,0 +1,45 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 com.cloud.utils.nio; + +public class TrustAllManager implements javax.net.ssl.TrustManager, javax.net.ssl.X509TrustManager { + @Override + public java.security.cert.X509Certificate[] getAcceptedIssuers() { + return null; + } + + public boolean isServerTrusted(java.security.cert.X509Certificate[] certs) { + return true; + } + + public boolean isClientTrusted(java.security.cert.X509Certificate[] certs) { + return true; + } + + @Override + public void checkServerTrusted(java.security.cert.X509Certificate[] certs, String authType) throws java.security.cert.CertificateException { + return; + } + + @Override + public void checkClientTrusted(java.security.cert.X509Certificate[] certs, String authType) throws java.security.cert.CertificateException { + return; + } +} http://git-wip-us.apache.org/repos/asf/cloudstack/blob/83fd8f60/utils/src/main/java/com/cloud/utils/rest/BasicEncodedRESTValidationStrategy.java ---------------------------------------------------------------------- diff --git a/utils/src/main/java/com/cloud/utils/rest/BasicEncodedRESTValidationStrategy.java b/utils/src/main/java/com/cloud/utils/rest/BasicEncodedRESTValidationStrategy.java new file mode 100644 index 0000000..bf001cd --- /dev/null +++ b/utils/src/main/java/com/cloud/utils/rest/BasicEncodedRESTValidationStrategy.java @@ -0,0 +1,66 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 com.cloud.utils.rest; + +import java.io.IOException; + +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.httpclient.HttpClient; +import org.apache.commons.httpclient.HttpException; +import org.apache.commons.httpclient.HttpMethodBase; + +/** + * Base 64 encoded authorization strategy. This implementation as opposed to + * {@link RESTValidationStrategy} doesn't do a login after auth error, but instead + * includes the encoded credentials in each request, instead of a cookie. + */ +public class BasicEncodedRESTValidationStrategy extends RESTValidationStrategy { + + public BasicEncodedRESTValidationStrategy(final String host, final String adminuser, final String adminpass) { + super(); + this.host = host; + user = adminuser; + password = adminpass; + } + + public BasicEncodedRESTValidationStrategy() { + } + + @Override + public void executeMethod(final HttpMethodBase method, final HttpClient client, + final String protocol) + throws CloudstackRESTException, HttpException, IOException { + if (host == null || host.isEmpty() || user == null || user.isEmpty() || password == null || password.isEmpty()) { + throw new CloudstackRESTException("Hostname/credentials are null or empty"); + } + + final String encodedCredentials = encodeCredentials(); + method.setRequestHeader("Authorization", "Basic " + encodedCredentials); + client.executeMethod(method); + } + + private String encodeCredentials() { + final String authString = user + ":" + password; + final byte[] authEncBytes = Base64.encodeBase64(authString.getBytes()); + final String authStringEnc = new String(authEncBytes); + return authStringEnc; + } + +} http://git-wip-us.apache.org/repos/asf/cloudstack/blob/83fd8f60/utils/src/main/java/com/cloud/utils/rest/CloudstackRESTException.java ---------------------------------------------------------------------- diff --git a/utils/src/main/java/com/cloud/utils/rest/CloudstackRESTException.java b/utils/src/main/java/com/cloud/utils/rest/CloudstackRESTException.java new file mode 100644 index 0000000..5985fa0 --- /dev/null +++ b/utils/src/main/java/com/cloud/utils/rest/CloudstackRESTException.java @@ -0,0 +1,39 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 com.cloud.utils.rest; + +public class CloudstackRESTException extends Exception { + + public CloudstackRESTException() { + } + + public CloudstackRESTException(final String message) { + super(message); + } + + public CloudstackRESTException(final Throwable cause) { + super(cause); + } + + public CloudstackRESTException(final String message, final Throwable cause) { + super(message, cause); + } + +} http://git-wip-us.apache.org/repos/asf/cloudstack/blob/83fd8f60/utils/src/main/java/com/cloud/utils/rest/RESTServiceConnector.java ---------------------------------------------------------------------- diff --git a/utils/src/main/java/com/cloud/utils/rest/RESTServiceConnector.java b/utils/src/main/java/com/cloud/utils/rest/RESTServiceConnector.java new file mode 100644 index 0000000..6ededcb --- /dev/null +++ b/utils/src/main/java/com/cloud/utils/rest/RESTServiceConnector.java @@ -0,0 +1,395 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 com.cloud.utils.rest; + +import com.google.gson.FieldNamingPolicy; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonDeserializer; +import com.google.gson.reflect.TypeToken; +import org.apache.cloudstack.utils.security.SSLUtils; +import org.apache.cloudstack.utils.security.SecureSSLSocketFactory; +import org.apache.commons.httpclient.ConnectTimeoutException; +import org.apache.commons.httpclient.HttpClient; +import org.apache.commons.httpclient.HttpException; +import org.apache.commons.httpclient.HttpMethod; +import org.apache.commons.httpclient.HttpMethodBase; +import org.apache.commons.httpclient.HttpStatus; +import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager; +import org.apache.commons.httpclient.NameValuePair; +import org.apache.commons.httpclient.cookie.CookiePolicy; +import org.apache.commons.httpclient.methods.DeleteMethod; +import org.apache.commons.httpclient.methods.GetMethod; +import org.apache.commons.httpclient.methods.PostMethod; +import org.apache.commons.httpclient.methods.PutMethod; +import org.apache.commons.httpclient.methods.StringRequestEntity; +import org.apache.commons.httpclient.params.HttpConnectionParams; +import org.apache.commons.httpclient.protocol.Protocol; +import org.apache.commons.httpclient.protocol.ProtocolSocketFactory; +import org.apache.commons.httpclient.protocol.SecureProtocolSocketFactory; +import org.apache.log4j.Logger; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.lang.reflect.Type; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.MalformedURLException; +import java.net.Socket; +import java.net.URL; +import java.net.UnknownHostException; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +/** + * This abstraction encapsulates client side code for REST service communication. It encapsulates + * access in a delegate validation strategy. There may different implementations extending + * {@link RESTValidationStrategy}, and any of them should mention the needed data to work. + * + * This connector allows the use of {@link JsonDeserializer} for specific classes. You can provide + * in the constructor a list of classes and a list of deserializers for these classes. These should + * be a correlated so that Nth deserializer is correctly mapped to Nth class. + */ +public class RESTServiceConnector { + private static final String HTTPS = "https"; + protected static final String GET_METHOD_TYPE = "get"; + protected static final String DELETE_METHOD_TYPE = "delete"; + protected static final String PUT_METHOD_TYPE = "put"; + protected static final String POST_METHOD_TYPE = "post"; + private static final String TEXT_HTML_CONTENT_TYPE = "text/html"; + private static final String JSON_CONTENT_TYPE = "application/json"; + private static final String CONTENT_TYPE = "Content-Type"; + private static final int BODY_RESP_MAX_LEN = 1024; + private static final int HTTPS_PORT = 443; + + private static final Logger s_logger = Logger.getLogger(RESTServiceConnector.class); + + protected final static String protocol = HTTPS; + + private final static MultiThreadedHttpConnectionManager s_httpClientManager = new MultiThreadedHttpConnectionManager(); + + protected RESTValidationStrategy validation; + + private final HttpClient client; + + private final Gson gson; + + + /** + * Getter that may be needed only for test purpose + * + * @return + */ + public Gson getGson() { + return gson; + } + + public RESTServiceConnector(final RESTValidationStrategy validationStrategy) { + this(validationStrategy, null, null); + } + + public RESTServiceConnector(final RESTValidationStrategy validationStrategy, final List<Class<?>> classList, final List<JsonDeserializer<?>> deserializerList) { + validation = validationStrategy; + client = createHttpClient(); + client.getParams().setCookiePolicy(CookiePolicy.BROWSER_COMPATIBILITY); + + try { + // Cast to ProtocolSocketFactory to avoid the deprecated constructor with the SecureProtocolSocketFactory parameter + Protocol.registerProtocol(HTTPS, new Protocol(HTTPS, (ProtocolSocketFactory)new TrustingProtocolSocketFactory(), HTTPS_PORT)); + } catch (final IOException e) { + s_logger.warn("Failed to register the TrustingProtocolSocketFactory, falling back to default SSLSocketFactory", e); + } + + final GsonBuilder gsonBuilder = new GsonBuilder(); + if(classList != null && deserializerList != null) { + for(int i = 0; i < classList.size() && i < deserializerList.size(); i++) { + gsonBuilder.registerTypeAdapter(classList.get(i), deserializerList.get(i)); + } + } + gson = gsonBuilder.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create(); + } + + public HttpClient createHttpClient() { + return new HttpClient(s_httpClientManager); + } + + public HttpMethod createMethod(final String type, final String uri) throws CloudstackRESTException { + String url; + try { + url = new URL(protocol, validation.getHost(), uri).toString(); + } catch (final MalformedURLException e) { + s_logger.error("Unable to build REST Service URL", e); + throw new CloudstackRESTException("Unable to build Nicira API URL", e); + } + + if (POST_METHOD_TYPE.equalsIgnoreCase(type)) { + return new PostMethod(url); + } else if (GET_METHOD_TYPE.equalsIgnoreCase(type)) { + return new GetMethod(url); + } else if (DELETE_METHOD_TYPE.equalsIgnoreCase(type)) { + return new DeleteMethod(url); + } else if (PUT_METHOD_TYPE.equalsIgnoreCase(type)) { + return new PutMethod(url); + } else { + throw new CloudstackRESTException("Requesting unknown method type"); + } + } + + public void setControllerAddress(final String address) { + validation.setHost(address); + } + + public void setAdminCredentials(final String username, final String password) { + validation.setUser(username); + validation.setPassword(password); + } + + public <T> void executeUpdateObject(final T newObject, final String uri, final Map<String, String> parameters) throws CloudstackRESTException { + + final PutMethod pm = (PutMethod)createMethod(PUT_METHOD_TYPE, uri); + pm.setRequestHeader(CONTENT_TYPE, JSON_CONTENT_TYPE); + try { + pm.setRequestEntity(new StringRequestEntity(gson.toJson(newObject), JSON_CONTENT_TYPE, null)); + } catch (final UnsupportedEncodingException e) { + throw new CloudstackRESTException("Failed to encode json request body", e); + } + + executeMethod(pm); + + if (pm.getStatusCode() != HttpStatus.SC_OK) { + final String errorMessage = responseToErrorMessage(pm); + pm.releaseConnection(); + s_logger.error("Failed to update object : " + errorMessage); + throw new CloudstackRESTException("Failed to update object : " + errorMessage); + } + pm.releaseConnection(); + } + + @SuppressWarnings("unchecked") + public <T> T executeCreateObject(final T newObject, final Type returnObjectType, final String uri, final Map<String, String> parameters) + throws CloudstackRESTException { + + final PostMethod pm = (PostMethod)createMethod(POST_METHOD_TYPE, uri); + pm.setRequestHeader(CONTENT_TYPE, JSON_CONTENT_TYPE); + try { + pm.setRequestEntity(new StringRequestEntity(gson.toJson(newObject), JSON_CONTENT_TYPE, null)); + } catch (final UnsupportedEncodingException e) { + throw new CloudstackRESTException("Failed to encode json request body", e); + } + + executeMethod(pm); + + if (pm.getStatusCode() != HttpStatus.SC_CREATED) { + final String errorMessage = responseToErrorMessage(pm); + pm.releaseConnection(); + s_logger.error("Failed to create object : " + errorMessage); + throw new CloudstackRESTException("Failed to create object : " + errorMessage); + } + + T result; + try { + result = (T)gson.fromJson(pm.getResponseBodyAsString(), TypeToken.get(newObject.getClass()).getType()); + } catch (final IOException e) { + throw new CloudstackRESTException("Failed to decode json response body", e); + } finally { + pm.releaseConnection(); + } + + return result; + } + + public void executeDeleteObject(final String uri) throws CloudstackRESTException { + final DeleteMethod dm = (DeleteMethod)createMethod(DELETE_METHOD_TYPE, uri); + dm.setRequestHeader(CONTENT_TYPE, JSON_CONTENT_TYPE); + + executeMethod(dm); + + if (dm.getStatusCode() != HttpStatus.SC_NO_CONTENT) { + final String errorMessage = responseToErrorMessage(dm); + dm.releaseConnection(); + s_logger.error("Failed to delete object : " + errorMessage); + throw new CloudstackRESTException("Failed to delete object : " + errorMessage); + } + dm.releaseConnection(); + } + + @SuppressWarnings("unchecked") + public <T> T executeRetrieveObject(final Type returnObjectType, final String uri, final Map<String, String> parameters) throws CloudstackRESTException { + final GetMethod gm = (GetMethod)createMethod(GET_METHOD_TYPE, uri); + gm.setRequestHeader(CONTENT_TYPE, JSON_CONTENT_TYPE); + if (parameters != null && !parameters.isEmpty()) { + final List<NameValuePair> nameValuePairs = new ArrayList<NameValuePair>(parameters.size()); + for (final Entry<String, String> e : parameters.entrySet()) { + nameValuePairs.add(new NameValuePair(e.getKey(), e.getValue())); + } + gm.setQueryString(nameValuePairs.toArray(new NameValuePair[0])); + } + + executeMethod(gm); + + if (gm.getStatusCode() != HttpStatus.SC_OK) { + final String errorMessage = responseToErrorMessage(gm); + gm.releaseConnection(); + s_logger.error("Failed to retrieve object : " + errorMessage); + throw new CloudstackRESTException("Failed to retrieve object : " + errorMessage); + } + + T returnValue; + try { + returnValue = (T)gson.fromJson(gm.getResponseBodyAsString(), returnObjectType); + } catch (final IOException e) { + s_logger.error("IOException while retrieving response body", e); + throw new CloudstackRESTException(e); + } finally { + gm.releaseConnection(); + } + return returnValue; + } + + public void executeMethod(final HttpMethodBase method) throws CloudstackRESTException { + try { + validation.executeMethod(method, client, protocol); + } catch (final HttpException e) { + s_logger.error("HttpException caught while trying to connect to the REST Service", e); + method.releaseConnection(); + throw new CloudstackRESTException("API call to REST Service Failed", e); + } catch (final IOException e) { + s_logger.error("IOException caught while trying to connect to the REST Service", e); + method.releaseConnection(); + throw new CloudstackRESTException("API call to Nicira REST Service Failed", e); + } + } + + private String responseToErrorMessage(final HttpMethodBase method) { + assert method.isRequestSent() : "no use getting an error message unless the request is sent"; + + if (TEXT_HTML_CONTENT_TYPE.equals(method.getResponseHeader(CONTENT_TYPE).getValue())) { + // The error message is the response content + // Safety margin of 1024 characters, anything longer is probably useless + // and will clutter the logs + try { + return method.getResponseBodyAsString(BODY_RESP_MAX_LEN); + } catch (final IOException e) { + s_logger.debug("Error while loading response body", e); + } + } + + // The default + return method.getStatusText(); + } + + /* Some controllers use a self-signed certificate. The + * TrustingProtocolSocketFactory will accept any provided + * certificate when making an SSL connection to the SDN + * Manager + */ + private class TrustingProtocolSocketFactory implements SecureProtocolSocketFactory { + + private SSLSocketFactory ssf; + + public TrustingProtocolSocketFactory() throws IOException { + // Create a trust manager that does not validate certificate chains + final TrustManager[] trustAllCerts = new TrustManager[] {new X509TrustManager() { + @Override + public X509Certificate[] getAcceptedIssuers() { + return null; + } + + @Override + public void checkClientTrusted(final X509Certificate[] certs, final String authType) { + // Trust always + } + + @Override + public void checkServerTrusted(final X509Certificate[] certs, final String authType) { + // Trust always + } + }}; + + try { + // Install the all-trusting trust manager + final SSLContext sc = SSLUtils.getSSLContext(); + sc.init(null, trustAllCerts, new java.security.SecureRandom()); + ssf = new SecureSSLSocketFactory(sc); + } catch (final KeyManagementException e) { + throw new IOException(e); + } catch (final NoSuchAlgorithmException e) { + throw new IOException(e); + } + } + + @Override + public Socket createSocket(final String host, final int port) throws IOException { + SSLSocket socket = (SSLSocket) ssf.createSocket(host, port); + socket.setEnabledProtocols(SSLUtils.getSupportedProtocols(socket.getEnabledProtocols())); + return socket; + } + + @Override + public Socket createSocket(final String address, final int port, final InetAddress localAddress, final int localPort) throws IOException, UnknownHostException { + Socket socket = ssf.createSocket(address, port, localAddress, localPort); + if (socket instanceof SSLSocket) { + ((SSLSocket)socket).setEnabledProtocols(SSLUtils.getSupportedProtocols(((SSLSocket)socket).getEnabledProtocols())); + } + return socket; + } + + @Override + public Socket createSocket(final Socket socket, final String host, final int port, final boolean autoClose) throws IOException, UnknownHostException { + Socket s = ssf.createSocket(socket, host, port, autoClose); + if (s instanceof SSLSocket) { + ((SSLSocket)s).setEnabledProtocols(SSLUtils.getSupportedProtocols(((SSLSocket)s).getEnabledProtocols())); + } + return s; + } + + @Override + public Socket createSocket(final String host, final int port, final InetAddress localAddress, final int localPort, final HttpConnectionParams params) + throws IOException, UnknownHostException, ConnectTimeoutException { + final int timeout = params.getConnectionTimeout(); + if (timeout == 0) { + Socket socket = createSocket(host, port, localAddress, localPort); + if (socket instanceof SSLSocket) { + ((SSLSocket)socket).setEnabledProtocols(SSLUtils.getSupportedProtocols(((SSLSocket)socket).getEnabledProtocols())); + } + return socket; + } else { + final Socket s = ssf.createSocket(); + if (s instanceof SSLSocket) { + ((SSLSocket)s).setEnabledProtocols(SSLUtils.getSupportedProtocols(((SSLSocket)s).getEnabledProtocols())); + } + s.bind(new InetSocketAddress(localAddress, localPort)); + s.connect(new InetSocketAddress(host, port), timeout); + return s; + } + } + } + +} http://git-wip-us.apache.org/repos/asf/cloudstack/blob/83fd8f60/utils/src/main/java/com/cloud/utils/rest/RESTValidationStrategy.java ---------------------------------------------------------------------- diff --git a/utils/src/main/java/com/cloud/utils/rest/RESTValidationStrategy.java b/utils/src/main/java/com/cloud/utils/rest/RESTValidationStrategy.java new file mode 100644 index 0000000..77ac8d0 --- /dev/null +++ b/utils/src/main/java/com/cloud/utils/rest/RESTValidationStrategy.java @@ -0,0 +1,165 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 com.cloud.utils.rest; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; + +import org.apache.commons.httpclient.HttpClient; +import org.apache.commons.httpclient.HttpException; +import org.apache.commons.httpclient.HttpMethodBase; +import org.apache.commons.httpclient.HttpStatus; +import org.apache.commons.httpclient.methods.PostMethod; +import org.apache.log4j.Logger; + +/** + * Basic authentication strategy. This strategy needs user and password for authentication. + * + * A login URL is needed which will be used for login and getting the cookie to be + * used in next requests. If an executeMethod request fails due to authorization it will try + * to login, get the cookie and repeat the attempt to execute the method. + */ +public class RESTValidationStrategy { + + private static final Logger s_logger = Logger.getLogger(RESTValidationStrategy.class); + + protected String host; + protected String user; + protected String password; + protected String serverVersion; + protected String loginUrl; + + public RESTValidationStrategy(final String host, final String user, final String password, + final String serverVersion, final String loginUrl) { + super(); + this.host = host; + this.user = user; + this.password = password; + this.serverVersion = serverVersion; + this.loginUrl = loginUrl; + } + + public RESTValidationStrategy(final String loginUrl) { + this.loginUrl = loginUrl; + } + + public RESTValidationStrategy() { + } + + public String getUser() { + return user; + } + + public void setUser(final String user) { + this.user = user; + } + + public String getPassword() { + return password; + } + + public void setPassword(final String password) { + this.password = password; + } + + public String getLoginUrl() { + return loginUrl; + } + + public void setLoginUrl(final String loginUrl) { + this.loginUrl = loginUrl; + } + + public String getHost() { + return host; + } + + public void setHost(final String host) { + this.host = host; + } + + public void executeMethod(final HttpMethodBase method, final HttpClient client, + final String protocol) + throws CloudstackRESTException, HttpException, IOException { + if (host == null || host.isEmpty() || user == null || user.isEmpty() || password == null || password.isEmpty()) { + throw new CloudstackRESTException("Hostname/credentials are null or empty"); + } + + client.executeMethod(method); + if (method.getStatusCode() == HttpStatus.SC_UNAUTHORIZED) { + method.releaseConnection(); + // login and try again + login(protocol, client); + client.executeMethod(method); + } + } + + /** + * Logs against the REST server. The cookie is stored in the <code>_authcookie<code> variable. + * <p> + * The method returns false if the login failed or the connection could not be made. + * + */ + protected void login(final String protocol, + final HttpClient client) + throws CloudstackRESTException { + String url; + + if (host == null || host.isEmpty() || user == null || user.isEmpty() || password == null || password.isEmpty()) { + throw new CloudstackRESTException("Hostname/credentials are null or empty"); + } + + try { + url = new URL(protocol, host, loginUrl).toString(); + } catch (final MalformedURLException e) { + s_logger.error("Unable to build Nicira API URL", e); + throw new CloudstackRESTException("Unable to build Nicira API URL", e); + } + + final PostMethod pm = new PostMethod(url); + pm.addParameter("username", user); + pm.addParameter("password", password); + + try { + client.executeMethod(pm); + } catch (final HttpException e) { + throw new CloudstackRESTException("REST Service API login failed ", e); + } catch (final IOException e) { + throw new CloudstackRESTException("REST Service API login failed ", e); + } finally { + pm.releaseConnection(); + } + + if (pm.getStatusCode() != HttpStatus.SC_OK) { + s_logger.error("REST Service API login failed : " + pm.getStatusText()); + throw new CloudstackRESTException("REST Service API login failed " + pm.getStatusText()); + } + + // Extract the version for later use + if (pm.getResponseHeader("Server") != null) { + serverVersion = pm.getResponseHeader("Server").getValue(); + s_logger.debug("Server reports version " + serverVersion); + } + + // Success; the cookie required for login is kept in _client + } + +} http://git-wip-us.apache.org/repos/asf/cloudstack/blob/83fd8f60/utils/src/main/java/com/cloud/utils/script/OutputInterpreter.java ---------------------------------------------------------------------- diff --git a/utils/src/main/java/com/cloud/utils/script/OutputInterpreter.java b/utils/src/main/java/com/cloud/utils/script/OutputInterpreter.java new file mode 100644 index 0000000..654f87e --- /dev/null +++ b/utils/src/main/java/com/cloud/utils/script/OutputInterpreter.java @@ -0,0 +1,141 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 com.cloud.utils.script; + +import java.io.BufferedReader; +import java.io.IOException; + +import org.apache.log4j.Logger; + +/** + */ +public abstract class OutputInterpreter { + public boolean drain() { + return false; + } + + public String processError(BufferedReader reader) throws IOException { + StringBuilder buff = new StringBuilder(); + String line = null; + while ((line = reader.readLine()) != null) { + buff.append(line); + } + return buff.toString(); + } + + public abstract String interpret(BufferedReader reader) throws IOException; + + public static final OutputInterpreter NoOutputParser = new OutputInterpreter() { + @Override + public String interpret(BufferedReader reader) throws IOException { + return null; + } + }; + + public static class TimedOutLogger extends OutputInterpreter { + private static final Logger s_logger = Logger.getLogger(TimedOutLogger.class); + Process _process; + + public TimedOutLogger(Process process) { + _process = process; + } + + @Override + public boolean drain() { + return true; + } + + @Override + public String interpret(BufferedReader reader) throws IOException { + StringBuilder buff = new StringBuilder(); + + while (reader.ready()) { + buff.append(reader.readLine()); + } + + _process.destroy(); + + try { + while (reader.ready()) { + buff.append(reader.readLine()); + } + } catch (IOException e) { + s_logger.info("[ignored] can not append line to buffer",e); + } + + return buff.toString(); + } + } + + public static class OutputLogger extends OutputInterpreter { + Logger _logger; + + public OutputLogger(Logger logger) { + _logger = logger; + } + + @Override + public String interpret(BufferedReader reader) throws IOException { + StringBuilder builder = new StringBuilder(); + String line = null; + while ((line = reader.readLine()) != null) { + builder.append(line).append("\n"); + } + if (builder.length() > 0) { + _logger.debug(builder.toString()); + } + return null; + } + } + + public static class OneLineParser extends OutputInterpreter { + String line = null; + + @Override + public String interpret(BufferedReader reader) throws IOException { + line = reader.readLine(); + return null; + } + + public String getLine() { + return line; + } + }; + + public static class AllLinesParser extends OutputInterpreter { + String allLines = null; + + @Override + public String interpret(BufferedReader reader) throws IOException { + StringBuilder builder = new StringBuilder(); + String line = null; + while ((line = reader.readLine()) != null) { + builder.append(line).append("\n"); + } + allLines = builder.toString(); + return null; + } + + public String getLines() { + return allLines; + } + } + +} http://git-wip-us.apache.org/repos/asf/cloudstack/blob/83fd8f60/utils/src/main/java/com/cloud/utils/script/Script.java ---------------------------------------------------------------------- diff --git a/utils/src/main/java/com/cloud/utils/script/Script.java b/utils/src/main/java/com/cloud/utils/script/Script.java new file mode 100644 index 0000000..487c62c --- /dev/null +++ b/utils/src/main/java/com/cloud/utils/script/Script.java @@ -0,0 +1,502 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 com.cloud.utils.script; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; +import java.util.concurrent.Callable; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.apache.commons.io.IOUtils; +import org.apache.log4j.Logger; + +import com.cloud.utils.PropertiesUtil; +import com.cloud.utils.concurrency.NamedThreadFactory; +import com.cloud.utils.script.OutputInterpreter.TimedOutLogger; + +public class Script implements Callable<String> { + private static final Logger s_logger = Logger.getLogger(Script.class); + + private final Logger _logger; + + public static final String ERR_EXECUTE = "execute.error"; + public static final String ERR_TIMEOUT = "timeout"; + private int _defaultTimeout = 3600 * 1000; /* 1 hour */ + private volatile boolean _isTimeOut = false; + + private boolean _passwordCommand = false; + + private static final ScheduledExecutorService s_executors = Executors.newScheduledThreadPool(10, new NamedThreadFactory("Script")); + + String _workDir; + ArrayList<String> _command; + long _timeout; + Process _process; + Thread _thread; + + public int getExitValue() { + return _process.exitValue(); + } + + public Script(String command, long timeout, Logger logger) { + _command = new ArrayList<String>(); + _command.add(command); + _timeout = timeout; + if (_timeout == 0) { + /* always using default timeout 1 hour to avoid thread hang */ + _timeout = _defaultTimeout; + } + _process = null; + _logger = logger != null ? logger : s_logger; + } + + public Script(boolean runWithSudo, String command, long timeout, Logger logger) { + this(command, timeout, logger); + if (runWithSudo) { + _command.add(0, "sudo"); + } + } + + public Script(String command, Logger logger) { + this(command, 0, logger); + } + + public Script(String command) { + this(command, 0, s_logger); + } + + public Script(String command, long timeout) { + this(command, timeout, s_logger); + } + + public void add(String... params) { + for (String param : params) { + _command.add(param); + } + } + + public void add(String param) { + _command.add(param); + } + + public Script set(String name, String value) { + _command.add(name); + _command.add(value); + return this; + } + + public void setWorkDir(String workDir) { + _workDir = workDir; + } + + protected String buildCommandLine(String[] command) { + StringBuilder builder = new StringBuilder(); + boolean obscureParam = false; + for (int i = 0; i < command.length; i++) { + String cmd = command[i]; + if (obscureParam) { + builder.append("******").append(" "); + obscureParam = false; + } else { + builder.append(command[i]).append(" "); + } + + if ("-y".equals(cmd) || "-z".equals(cmd)) { + obscureParam = true; + _passwordCommand = true; + } + } + return builder.toString(); + } + + protected String buildCommandLine(List<String> command) { + StringBuilder builder = new StringBuilder(); + boolean obscureParam = false; + for (String cmd : command) { + if (obscureParam) { + builder.append("******").append(" "); + obscureParam = false; + } else { + builder.append(cmd).append(" "); + } + + if ("-y".equals(cmd) || "-z".equals(cmd)) { + obscureParam = true; + _passwordCommand = true; + } + } + return builder.toString(); + } + + public String execute() { + return execute(new OutputInterpreter.OutputLogger(_logger)); + } + + @Override + public String toString() { + String[] command = _command.toArray(new String[_command.size()]); + return buildCommandLine(command); + } + + static String stackTraceAsString(Throwable throwable) { + //TODO: a StringWriter is bit to heavy weight + try(StringWriter out = new StringWriter(); PrintWriter writer = new PrintWriter(out);) { + throwable.printStackTrace(writer); + return out.toString(); + } catch (IOException e) { + return ""; + } + } + + public String execute(OutputInterpreter interpreter) { + String[] command = _command.toArray(new String[_command.size()]); + + if (_logger.isDebugEnabled()) { + _logger.debug("Executing: " + buildCommandLine(command)); + } + + try { + ProcessBuilder pb = new ProcessBuilder(command); + pb.redirectErrorStream(true); + if (_workDir != null) + pb.directory(new File(_workDir)); + + _process = pb.start(); + if (_process == null) { + _logger.warn("Unable to execute: " + buildCommandLine(command)); + return "Unable to execute the command: " + command[0]; + } + + BufferedReader ir = new BufferedReader(new InputStreamReader(_process.getInputStream())); + + _thread = Thread.currentThread(); + ScheduledFuture<String> future = null; + if (_timeout > 0) { + future = s_executors.schedule(this, _timeout, TimeUnit.MILLISECONDS); + } + + Task task = null; + if (interpreter != null && interpreter.drain()) { + task = new Task(interpreter, ir); + s_executors.execute(task); + } + + while (true) { + try { + if (_process.waitFor() == 0) { + _logger.debug("Execution is successful."); + if (interpreter != null) { + return interpreter.drain() ? task.getResult() : interpreter.interpret(ir); + } else { + // null return exitValue apparently + return String.valueOf(_process.exitValue()); + } + } else { + break; + } + } catch (InterruptedException e) { + if (!_isTimeOut) { + /* + * This is not timeout, we are interrupted by others, + * continue + */ + _logger.debug("We are interrupted but it's not a timeout, just continue"); + continue; + } + + TimedOutLogger log = new TimedOutLogger(_process); + Task timedoutTask = new Task(log, ir); + + timedoutTask.run(); + if (!_passwordCommand) { + _logger.warn("Timed out: " + buildCommandLine(command) + ". Output is: " + timedoutTask.getResult()); + } else { + _logger.warn("Timed out: " + buildCommandLine(command)); + } + + return ERR_TIMEOUT; + } finally { + if (future != null) { + future.cancel(false); + } + Thread.interrupted(); + } + } + + _logger.debug("Exit value is " + _process.exitValue()); + + BufferedReader reader = new BufferedReader(new InputStreamReader(_process.getInputStream()), 128); + + String error; + if (interpreter != null) { + error = interpreter.processError(reader); + } else { + error = String.valueOf(_process.exitValue()); + } + + if (_logger.isDebugEnabled()) { + _logger.debug(error); + } + return error; + } catch (SecurityException ex) { + _logger.warn("Security Exception....not running as root?", ex); + return stackTraceAsString(ex); + } catch (Exception ex) { + _logger.warn("Exception: " + buildCommandLine(command), ex); + return stackTraceAsString(ex); + } finally { + if (_process != null) { + IOUtils.closeQuietly(_process.getErrorStream()); + IOUtils.closeQuietly(_process.getOutputStream()); + IOUtils.closeQuietly(_process.getInputStream()); + _process.destroy(); + } + } + } + + @Override + public String call() { + try { + _logger.trace("Checking exit value of process"); + _process.exitValue(); + _logger.trace("Script ran within the alloted time"); + } catch (IllegalThreadStateException e) { + _logger.warn("Interrupting script."); + _isTimeOut = true; + _thread.interrupt(); + } + return null; + } + + public static class Task implements Runnable { + OutputInterpreter interpreter; + BufferedReader reader; + String result; + boolean done; + + public Task(OutputInterpreter interpreter, BufferedReader reader) { + this.interpreter = interpreter; + this.reader = reader; + result = null; + } + + @Override + public void run() { + synchronized(this) { + done = false; + try { + result = interpreter.interpret(reader); + } catch (IOException ex) { + result = stackTraceAsString(ex); + } catch (Exception ex) { + result = stackTraceAsString(ex); + } finally { + done = true; + notifyAll(); + IOUtils.closeQuietly(reader); + } + } + } + + public synchronized String getResult() throws InterruptedException { + if (!done) { + wait(); + } + return result; + } + } + + public static String findScript(String path, String script) { + s_logger.debug("Looking for " + script + " in the classpath"); + + URL url = ClassLoader.getSystemResource(script); + s_logger.debug("System resource: " + url); + File file = null; + if (url != null) { + file = new File(url.getFile()); + s_logger.debug("Absolute path = " + file.getAbsolutePath()); + return file.getAbsolutePath(); + } + + if (path == null) { + s_logger.warn("No search path specified, unable to look for " + script); + return null; + } + path = path.replace("/", File.separator); + + /** + * Look in WEB-INF/classes of the webapp + * URI workaround the URL encoding of url.getFile + */ + if (path.endsWith(File.separator)) { + url = Script.class.getClassLoader().getResource(path + script); + } else { + url = Script.class.getClassLoader().getResource(path + File.separator + script); + } + s_logger.debug("Classpath resource: " + url); + if (url != null) { + try { + file = new File(new URI(url.toString()).getPath()); + s_logger.debug("Absolute path = " + file.getAbsolutePath()); + return file.getAbsolutePath(); + } catch (URISyntaxException e) { + s_logger.warn("Unable to convert " + url.toString() + " to a URI"); + } + } + + if (path.endsWith(File.separator)) { + path = path.substring(0, path.lastIndexOf(File.separator)); + } + + if (path.startsWith(File.separator)) { + // Path given was absolute so we assume the caller knows what they want. + file = new File(path + File.separator + script); + return file.exists() ? file.getAbsolutePath() : null; + } + + s_logger.debug("Looking for " + script); + String search = null; + for (int i = 0; i < 3; i++) { + if (i == 0) { + String cp = Script.class.getResource(Script.class.getSimpleName() + ".class").toExternalForm(); + int begin = cp.indexOf(File.separator); + + // work around with the inconsistency of java classpath and file separator on Windows 7 + if (begin < 0) + begin = cp.indexOf('/'); + + int endBang = cp.lastIndexOf("!"); + int end = cp.lastIndexOf(File.separator, endBang); + if (end < 0) + end = cp.lastIndexOf('/', endBang); + if (end < 0) + cp = cp.substring(begin); + else + cp = cp.substring(begin, end); + + s_logger.debug("Current binaries reside at " + cp); + search = cp; + } else if (i == 1) { + s_logger.debug("Searching in environment.properties"); + try { + final File propsFile = PropertiesUtil.findConfigFile("environment.properties"); + if (propsFile == null) { + s_logger.debug("environment.properties could not be opened"); + } else { + final Properties props = PropertiesUtil.loadFromFile(propsFile); + search = props.getProperty("paths.script"); + } + } catch (IOException e) { + s_logger.debug("environment.properties could not be opened"); + continue; + } + s_logger.debug("environment.properties says scripts should be in " + search); + } else { + s_logger.debug("Searching in the current directory"); + search = "."; + } + + search += File.separatorChar + path + File.separator; + do { + search = search.substring(0, search.lastIndexOf(File.separator)); + file = new File(search + File.separator + script); + s_logger.debug("Looking for " + script + " in " + file.getAbsolutePath()); + } while (!file.exists() && search.lastIndexOf(File.separator) != -1); + + if (file.exists()) { + return file.getAbsolutePath(); + } + + } + + search = System.getProperty("paths.script"); + + search += File.separatorChar + path + File.separator; + do { + search = search.substring(0, search.lastIndexOf(File.separator)); + file = new File(search + File.separator + script); + s_logger.debug("Looking for " + script + " in " + file.getAbsolutePath()); + } while (!file.exists() && search.lastIndexOf(File.separator) != -1); + + if (file.exists()) { + return file.getAbsolutePath(); + } + + s_logger.warn("Unable to find script " + script); + return null; + } + + public static String runSimpleBashScript(String command) { + return Script.runSimpleBashScript(command, 0); + } + + public static String runSimpleBashScript(String command, int timeout) { + + Script s = new Script("/bin/bash", timeout); + s.add("-c"); + s.add(command); + + OutputInterpreter.OneLineParser parser = new OutputInterpreter.OneLineParser(); + if (s.execute(parser) != null) + return null; + + String result = parser.getLine(); + if (result == null || result.trim().isEmpty()) + return null; + else + return result.trim(); + } + + public static int runSimpleBashScriptForExitValue(String command) { + return runSimpleBashScriptForExitValue(command, 0); + } + + public static int runSimpleBashScriptForExitValue(String command, int timeout) { + + Script s = new Script("/bin/bash", timeout); + s.add("-c"); + s.add(command); + + String result = s.execute(null); + if (result == null || result.trim().isEmpty()) + return -1; + else { + try { + return Integer.parseInt(result.trim()); + } catch (NumberFormatException e) { + return -1; + } + } + } + +} http://git-wip-us.apache.org/repos/asf/cloudstack/blob/83fd8f60/utils/src/main/java/com/cloud/utils/script/Script2.java ---------------------------------------------------------------------- diff --git a/utils/src/main/java/com/cloud/utils/script/Script2.java b/utils/src/main/java/com/cloud/utils/script/Script2.java new file mode 100644 index 0000000..03c0e0d --- /dev/null +++ b/utils/src/main/java/com/cloud/utils/script/Script2.java @@ -0,0 +1,70 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 com.cloud.utils.script; + +import java.util.HashMap; + +import org.apache.log4j.Logger; + +public class Script2 extends Script { + HashMap<String, ParamType> _params = new HashMap<String, ParamType>(); + + public static enum ParamType { + NORMAL, PASSWORD, + } + + public Script2(String command, Logger logger) { + this(command, 0, logger); + } + + public Script2(String command, long timeout, Logger logger) { + super(command, timeout, logger); + } + + public void add(String param, ParamType type) { + _params.put(param, type); + super.add(param); + } + + @Override + public void add(String param) { + add(param, ParamType.NORMAL); + } + + private ParamType getType(String cmd) { + return _params.get(cmd); + } + + @Override + protected String buildCommandLine(String[] command) { + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < command.length; i++) { + String cmd = command[i]; + ParamType type = getType(cmd); + if (type == ParamType.PASSWORD) { + builder.append("******").append(" "); + } else { + builder.append(command[i]).append(" "); + } + } + + return builder.toString(); + } +}