package io.eguan.nbdsrv.client;
/*
* #%L
* Project eguan
* %%
* Copyright (C) 2012 - 2017 Oodrive
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import io.eguan.nbdsrv.SocketHandle;
import io.eguan.nbdsrv.packet.DataPushingCmd;
import io.eguan.nbdsrv.packet.DataPushingPacket;
import io.eguan.nbdsrv.packet.DataPushingReplyPacket;
import io.eguan.nbdsrv.packet.NbdException;
import io.eguan.nbdsrv.packet.OptionCmd;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
final class NbdClient {
private enum Phase {
IDLE_PHASE, HANDSHAKE_PHASE, DATA_PUSHING_PHASE;
}
private static final Logger LOGGER = LoggerFactory.getLogger(Client.class);
/** Server address */
private final InetSocketAddress address;
/** Handle for the socket */
private SocketHandle socketHandle;
/** Export flags received from the server */
private long exportFlags;
/** Export Size receive from the server */
private long exportSize;
/** Export Name chosen */
private String exportName;
/** Global flags of the server */
private long globalRemoteFlags;
/** Current Phase */
private Phase phase;
private final ExecutorService executor = Executors.newSingleThreadExecutor();
private static final String[] EMPTY_STRING_ARRAY = new String[0];
NbdClient(final InetSocketAddress address) {
this.address = address;
this.phase = Phase.IDLE_PHASE;
}
/**
* Gets the export Flags.
*
* @return the export flags
*/
final long getExportFlags() {
return exportFlags;
}
/**
* Sets the export flags.
*
* @param exportFlags
* the flags to set
*/
final void setExportFlags(final long exportFlags) {
this.exportFlags = exportFlags;
}
/**
* Gets the export size.
*
* @return the export size
*/
final long getExportSize() {
return exportSize;
}
/**
* Sets the export size.
*
* @param exportSize
* the size to set
*/
final void setExportSize(final long exportSize) {
this.exportSize = exportSize;
}
/**
* Gets the export name chosen by the client for this connection.
*
* @return a String representing the name of this export
*/
final String getExportName() {
return this.exportName;
}
/**
* Set the export name chosen by the client to use for this connection.
*
* @param name
* represents the name of this export
*/
final void setExportName(final String name) {
this.exportName = name;
}
/**
* Gets the export remote flags.
*
* @return the flags of the remote export
*/
final long getGlobalRemoteFlags() {
return globalRemoteFlags;
}
/**
* Sets the global remote flags.
*
* @param globalRemoteFlags
* the flags to set
*/
final void setGlobalRemoteFlags(final long globalRemoteFlags) {
this.globalRemoteFlags = globalRemoteFlags;
}
/**
* Close the client.
*/
final void close() {
// Reset client info
phase = Phase.IDLE_PHASE;
setExportName("");
setExportSize(0);
setExportFlags(0);
try {
if (socketHandle != null) {
socketHandle.close();
}
}
catch (final Throwable t) {
LOGGER.warn("Failed to close socket", t);
}
}
/**
* Start the handshake.
*
* @throws IOException
* If an I/O error occurs
* @throws InterruptedException
* If the current thread was interrupted
* @throws NbdException
* If the NBD protocol is not respected
*/
final void handshake() throws IOException, InterruptedException, NbdException {
if (phase != Phase.IDLE_PHASE) {
throw new NbdException("Client not in a idle phase");
}
final SocketChannel clientSocketChannel = SocketChannel.open();
try {
clientSocketChannel.configureBlocking(true);
clientSocketChannel.connect(address);
clientSocketChannel.socket().setTcpNoDelay(true);
socketHandle = new SocketHandle(clientSocketChannel, null);
phase = Phase.HANDSHAKE_PHASE;
final Future<Void> future = executor.submit(new InitTask(this));
future.get();
}
catch (final IOException e) {
close();
throw e;
}
catch (final InterruptedException e) {
close();
throw e;
}
catch (final ExecutionException e) {
close();
LOGGER.error("Execution exception", e.getCause());
final Throwable t = e.getCause();
if (t instanceof NbdException) {
throw (NbdException) t;
}
else if (t instanceof IOException) {
throw (IOException) t;
}
}
}
/**
* Gets the list of the available exports during the handshake phase.
*
* @return An array with the name of the exports
*
* @throws IOException
* If an I/O error occurs
* @throws InterruptedException
* If the current thread was interrupted
* @throws NbdException
* If the NBD protocol is not respected
*/
final String[] handshakeGetList() throws IOException, InterruptedException, NbdException {
return doOptionNegociation(OptionCmd.NBD_OPT_LIST, null);
}
/**
* Connect client to an export during the handshake phase. No other options (getList or abortHandshake) can be used
* after that. This ends the handshake phase.
*
* @param name
* the name of the export to connect the client
* @throws IOException
* If an I/O error occurs
* @throws InterruptedException
* If the current thread was interrupted
* @throws NbdException
* If the NBD protocol is not respected
*/
final void handshakeExportName(final String exportName) throws IOException, InterruptedException, NbdException {
doOptionNegociation(OptionCmd.NBD_OPT_EXPORT_NAME, exportName);
}
/**
* Abort the handshake.
*
* @throws IOException
* If an I/O error occurs
* @throws InterruptedException
* If the current thread was interrupted
* @throws NbdException
* If the NBD protocol is not respected
*/
final void handshakeAbort() throws IOException, InterruptedException, NbdException {
doOptionNegociation(OptionCmd.NBD_OPT_ABORT, null);
}
/**
* Send an option to server during the handshake phase.
*
* @param cmd
* the option to send
* @param data
* the data to the send
* @return an array with the eventual answer from the server
*
* @throws IOException
* If an I/O error occurs
* @throws InterruptedException
* If the current thread was interrupted
* @throws NbdException
* If the NBD protocol is not respected
*/
private final String[] doOptionNegociation(final OptionCmd cmd, final String data) throws IOException,
InterruptedException, NbdException {
if (phase != Phase.HANDSHAKE_PHASE) {
throw new NbdException("Client not in a handshake phase");
}
final Future<String[]> future = executor.submit(new OptionNegotiationTask(this, cmd, data));
try {
final String[] result = future.get();
if (cmd == OptionCmd.NBD_OPT_EXPORT_NAME) {
phase = Phase.DATA_PUSHING_PHASE;
}
return result;
}
catch (final InterruptedException e) {
close();
throw e;
}
catch (final ExecutionException e) {
LOGGER.error("Execution exception", e.getCause());
close();
final Throwable t = e.getCause();
if (t instanceof NbdException) {
throw (NbdException) t;
}
else if (t instanceof IOException) {
throw (IOException) t;
}
}
return EMPTY_STRING_ARRAY;
}
/**
* Send a write request to the server if the client is in data pushing phase.
*
* @param buffer
* the buffer to transmit. The position and limit must be set correctly: the writing is done from the
* buffer.position() to the buffer.limit(). Then the position is reset to 0.
* @param offset
* the first byte to write in the server export
* @throws IOException
* If an I/O error occurs
* @throws InterruptedException
* If the current thread was interrupted
* @throws NbdException
* If the NBD protocol is not respected
*/
final void writeRequest(final ByteBuffer src, final long offset) throws IOException, InterruptedException,
NbdException {
if (phase != Phase.DATA_PUSHING_PHASE) {
throw new NbdException("Client not in a data pushing phase");
}
final Future<Boolean> future = executor.submit(new WriteTask(this, src, offset));
try {
future.get();
}
catch (final InterruptedException e) {
close();
throw e;
}
catch (final ExecutionException e) {
LOGGER.error("Execution exception", e.getCause());
close();
final Throwable t = e.getCause();
if (t instanceof NbdException) {
throw (NbdException) t;
}
else if (t instanceof IOException) {
throw (IOException) t;
}
}
}
/**
* Send a read request to the server if the client is in data pushing phase.
*
* @param buf
* the buffer to receive the data. Position and limit must set correctly : the reading is done from the
* buffer.position() to the buffer.limit(). Then the position is reset to 0.
* @param offset
* the position of the first byte to read in a server export
* @throws IOException
* If an I/O error occurs
* @throws InterruptedException
* If the current thread was interrupted
* @throws NbdException
* If the NBD protocol is not respected
*/
final void readRequest(final ByteBuffer dst, final long offset) throws IOException, NbdException,
InterruptedException {
if (phase != Phase.DATA_PUSHING_PHASE) {
throw new NbdException("Client not in a data pushing phase");
}
final Future<Boolean> future = executor.submit(new ReadTask(this, dst, offset));
try {
future.get();
}
catch (final InterruptedException e) {
close();
throw e;
}
catch (final ExecutionException e) {
close();
LOGGER.error("Execution exception", e.getCause());
final Throwable t = e.getCause();
if (t instanceof NbdException) {
throw (NbdException) t;
}
else if (t instanceof IOException) {
throw (IOException) t;
}
}
}
/**
* Send a trim request to the server if the client is in data pushing phase.
*
* @param offset
* the first byte to discard
* @param length
* the number of bytes to discard
* @throws NbdException
* @throws InterruptedException
* @throws IOException
*/
public final void trimRequest(final long offset, final int length) throws NbdException, InterruptedException,
IOException {
if (phase != Phase.DATA_PUSHING_PHASE) {
throw new NbdException("Client not in a data pushing phase");
}
final Future<Boolean> future = executor.submit(new TrimTask(this, offset, length));
try {
future.get();
}
catch (final InterruptedException e) {
close();
throw e;
}
catch (final ExecutionException e) {
close();
LOGGER.error("Execution exception", e.getCause());
final Throwable t = e.getCause();
if (t instanceof NbdException) {
throw (NbdException) t;
}
else if (t instanceof IOException) {
throw (IOException) t;
}
}
}
/**
* Disconnect the client in a data pushing phase.
*
* @throws IOException
* If an I/O error occurs
* @throws InterruptedException
* If the current thread was interrupted
* @throws NbdException
* If the NBD protocol is not respected
*/
final void disconnectRequest() throws IOException, NbdException, InterruptedException {
if (phase != Phase.DATA_PUSHING_PHASE) {
throw new NbdException("Client not in a data pushing phase");
}
final Future<Boolean> future = executor.submit(new DisconnectTask(this));
try {
future.get();
}
catch (final InterruptedException e) {
socketHandle.close();
throw e;
}
catch (final ExecutionException e) {
LOGGER.error("Execution exception", e.getCause());
close();
final Throwable t = e.getCause();
if (t instanceof NbdException) {
throw (NbdException) t;
}
else if (t instanceof IOException) {
throw (IOException) t;
}
}
}
/**
* Write data into the socket up to the limit of each buffer.
*
* @param src
* an array of {@link ByteBuffer} which contain the data
* @throws IOException
* If an I/O error occurs
*/
final long writeSocket(final ByteBuffer[] src) throws IOException {
return socketHandle.write(src);
}
/**
* Write data into the socket up to the limit of the buffer.
*
* @param src
* a {@link ByteBuffer} which contains the data
* @throws IOException
* If an I/O error occurs
*/
final long writeSocket(final ByteBuffer src) throws IOException {
return socketHandle.write(src);
}
/**
* Write data into the socket up to the limit of each buffer.
*
* @param dst
* a {@link ByteBuffer} to fill with received data
* @throws IOException
* If an I/O error occurs
*/
final long readSocket(final ByteBuffer[] dst) throws IOException {
return socketHandle.read(dst);
}
/**
* Read data in the socket and fill the buffer up to its limit.
*
* @param dst
* a {@link ByteBuffer} to fill with received data
* @throws IOException
* If an I/O error occurs
*/
final long readSocket(final ByteBuffer dst) throws IOException {
return socketHandle.read(dst);
}
/**
* Read with a too long parameter in length (higher than an signed integer)
*
* @param length
* @param offset
* @throws IOException
* @throws NbdException
*/
public void readTooLong() throws IOException, NbdException {
// Send write request
final DataPushingPacket dataPushingPacket = new DataPushingPacket(DataPushingPacket.MAGIC,
DataPushingCmd.NBD_CMD_READ, 0xFFFFFFF, 0, 2147483648L);
final ByteBuffer header = DataPushingPacket.serialize(dataPushingPacket);
try {
writeSocket(header);
}
finally {
DataPushingPacket.release(header);
}
// Wait Answer
LOGGER.debug("Read Data Pushing Reply");
final ByteBuffer reply = DataPushingReplyPacket.allocateHeader();
try {
readSocket(reply);
DataPushingReplyPacket.deserialize(reply);
}
finally {
DataPushingReplyPacket.release(reply);
}
}
}