package io.eguan.nbdsrv;
/*
* #%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.packet.DataPushingCmd;
import io.eguan.nbdsrv.packet.DataPushingError;
import io.eguan.nbdsrv.packet.DataPushingPacket;
import io.eguan.nbdsrv.packet.DataPushingReplyPacket;
import io.eguan.nbdsrv.packet.NbdByteBufferCache;
import io.eguan.nbdsrv.packet.NbdException;
import io.eguan.nbdsrv.packet.Utils;
import java.io.IOException;
import java.nio.ByteBuffer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Represents the data pushing phase.
*
* @author oodrive
* @author ebredzinski
* @author llambert
*
*/
final class DataPushingPhase extends PhaseAbstract {
private static final Logger LOGGER = LoggerFactory.getLogger(DataPushingPhase.class);
DataPushingPhase(final ClientConnection connection) {
super(connection);
}
@Override
public final boolean execute() throws IOException, NbdException {
final DataPushingPacket dataPacket = readDataPushingPacket();
final long length;
try {
length = checkArguments(dataPacket);
}
catch (final IllegalArgumentException e) {
LOGGER.error("Invalid argument", e);
return true;
}
switch (dataPacket.getType()) {
case NBD_CMD_READ:
handleRead(dataPacket.getFrom(), (int) length, dataPacket.getHandle());
break;
case NBD_CMD_WRITE:
handleWrite(dataPacket.getFrom(), (int) length, dataPacket.getHandle());
break;
case NBD_CMD_DISC:
LOGGER.debug("Receive NBD_CMD_DISC");
return false;
case NBD_CMD_TRIM:
handleTrim(dataPacket.getFrom(), length, dataPacket.getHandle());
break;
case NBD_CMD_FLUSH:
LOGGER.debug("Receive NBD_CMD_FLUSH");
break;
default:
LOGGER.error("Ignore not supported command ");
break;
}
return true;
}
/**
* Handle the reception of a NBD_CMD_TRIM.
*
* @param from
* the offset of the first byte to be trimmed
* @param len
* the number of bytes to trim
* @param handle
* the handle used to identify the request
* @throws IOException
* @throws NbdException
*/
private final void handleTrim(final long from, final long len, final long handle) throws IOException, NbdException {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("NBD_CMD_TRIM from " + from + " len " + len);
}
final ClientConnection connection = getConnection();
final NbdDevice device = connection.getNbdDevice();
if (device == null) {
throw new NbdException("Client not connected");
}
// Trim data in the device
if (connection.isTrimEnabled()) {
device.trim(len, from);
}
// Create reply, no data necessary
final DataPushingReplyPacket replyPacket = new DataPushingReplyPacket(DataPushingReplyPacket.MAGIC,
DataPushingError.NBD_NO_ERROR, handle);
final ByteBuffer replyBuffer = DataPushingReplyPacket.serialize(replyPacket);
try {
connection.write(replyBuffer);
}
finally {
DataPushingPacket.release(replyBuffer);
}
}
/**
* Handle the reception of a NBD_CMD_READ.
*
* @param from
* the offset of the first byte to be copied
* @param len
* the number of bytes to copy
* @param handle
* the handle used to identify the request
*
*/
private final void handleRead(final long from, final int len, final long handle) throws NbdException, IOException {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("NBD_CMD_READ from " + from + " len " + len);
}
final ClientConnection connection = getConnection();
final NbdDevice device = connection.getNbdDevice();
if (device == null) {
throw new NbdException("Client not connected");
}
final ByteBuffer body = NbdByteBufferCache.allocate(len);
try {
// Read data in the device
device.read(body, len, from);
body.flip();
// Create header
final DataPushingReplyPacket replyPacket = new DataPushingReplyPacket(DataPushingReplyPacket.MAGIC,
DataPushingError.NBD_NO_ERROR, handle);
final ByteBuffer header = DataPushingReplyPacket.serialize(replyPacket);
try {
// Send the two buffers with scatter gather
final ByteBuffer[] buffers = { header, body };
connection.write(buffers);
}
finally {
DataPushingPacket.release(header);
}
}
catch (final IOException e) {
LOGGER.error("I/O Exception thrown", e);
sendError(DataPushingError.NBD_IO_ERROR, connection, handle);
}
finally {
DataPushingPacket.release(body);
}
}
/**
* Check the arguments offset and length.
*
* @param DataPushingPacket
* the data pushing packet received
*
* @return the length as an integer if it is possible.
*
* @throws IllegalArgumentException
* if one of the parameter is illegal
* @throws IOException
* IF I/O exception occurs when writing reply in the socket
*/
private final long checkArguments(final DataPushingPacket dataPushingPacket) throws IOException {
final ClientConnection connection = getConnection();
final NbdDevice device = connection.getNbdDevice();
final long handle = dataPushingPacket.getHandle();
final long length;
// Check command
if (dataPushingPacket.getType() == null) {
// Ignore unknown command
throw new IllegalArgumentException("Unknown command");
}
// Check Magic number
if (dataPushingPacket.getMagic() != DataPushingPacket.MAGIC) {
sendError(DataPushingError.NBD_EINVAL_ERROR, connection, handle);
throw new IllegalArgumentException("Bad magic number=0x" + Long.toHexString(dataPushingPacket.getMagic()));
}
// Check length parameter (defined as an unsigned int in NBD protocol)
if (dataPushingPacket.getType() == DataPushingCmd.NBD_CMD_TRIM) {
// long is allowed for TRIM command
length = dataPushingPacket.getLen();
}
else {
// Convert len in positive int (only if possible). Long not allowed for READ/WRITE commands
try {
length = Utils.getUnsignedIntPositive(dataPushingPacket.getLen());
}
catch (final IllegalArgumentException e) {
sendError(DataPushingError.NBD_EINVAL_ERROR, connection, handle);
if (dataPushingPacket.getType() == DataPushingCmd.NBD_CMD_WRITE) {
skipData(dataPushingPacket.getLen());
}
throw new IllegalArgumentException("Length=" + dataPushingPacket.getLen()
+ " is not a positive integer");
}
}
// Check if from is positive
if (dataPushingPacket.getFrom() < 0) {
sendError(DataPushingError.NBD_EINVAL_ERROR, connection, handle);
if (dataPushingPacket.getType() == DataPushingCmd.NBD_CMD_WRITE) {
skipData(length);
}
throw new IllegalArgumentException("Offset=" + dataPushingPacket.getFrom() + " is not a positive long");
}
// Check the range
if (dataPushingPacket.getFrom() + length > device.getSize()) {
sendError(DataPushingError.NBD_EINVAL_ERROR, connection, handle);
if (dataPushingPacket.getType() == DataPushingCmd.NBD_CMD_WRITE) {
skipData(length);
}
throw new IllegalArgumentException("Illegal range: from=" + dataPushingPacket.getFrom() + " length="
+ length + " deviceSize=" + device.getSize());
}
return length;
}
/**
* Consume data without processing them.
*
* @param length
* the length to consume
* @throws IOException
*/
private final void skipData(final long length) throws IOException {
final ClientConnection connection = getConnection();
long skipLength = 0;
while (skipLength < length) {
final ByteBuffer bytes = NbdByteBufferCache.allocate(length - skipLength > 4096 ? 4096
: (int) (length - skipLength));
try {
// Read data into the socket
skipLength += connection.read(bytes);
}
finally {
NbdByteBufferCache.release(bytes);
}
}
}
/**
* Send an error.
*
* @param error
* error code to send
* @param connection
* the connection
* @param handle
* the handle
* @throws IOException
* if some i/o error occurs during socket writing
*/
private final void sendError(final DataPushingError error, final ClientConnection connection, final long handle)
throws IOException {
// Create header
final DataPushingReplyPacket replyPacket = new DataPushingReplyPacket(DataPushingReplyPacket.MAGIC, error,
handle);
final ByteBuffer header = DataPushingReplyPacket.serialize(replyPacket);
try {
// Send the two buffers with scatter gather
final ByteBuffer[] buffers = { header };
connection.write(buffers);
}
finally {
DataPushingPacket.release(header);
}
}
/**
* Handle the reception of a NBD_CMD_WRITE.
*
* @param from
* the offset of the first byte to store
* @param len
* the number of bytes to store
* @param handle
* the handle used to identify the request
* @throws IOException
*
*/
private final void handleWrite(final long from, final int len, final long handle) throws NbdException, IOException {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("NBD_CMD_WRITE from " + from + " len " + len);
}
final ClientConnection connection = getConnection();
final NbdDevice device = connection.getNbdDevice();
if (device == null) {
throw new NbdException("Client not connected");
}
final ByteBuffer bytes = NbdByteBufferCache.allocate(len);
try {
// Read data into the socket
connection.read(bytes);
// Write data on the device if not read-only
if (device.isReadOnly()) {
LOGGER.error("Write on read-only device not permitted");
sendError(DataPushingError.NBD_EPERM_ERROR, connection, handle);
}
else {
// Write them in the device
device.write(bytes, len, from);
// Create reply, no data necessary
final DataPushingReplyPacket replyPacket = new DataPushingReplyPacket(DataPushingReplyPacket.MAGIC,
DataPushingError.NBD_NO_ERROR, handle);
final ByteBuffer replyBuffer = DataPushingReplyPacket.serialize(replyPacket);
try {
connection.write(replyBuffer);
}
finally {
DataPushingPacket.release(replyBuffer);
}
}
}
catch (final IOException e) {
LOGGER.error("I/O Exception thrown", e);
sendError(DataPushingError.NBD_IO_ERROR, connection, handle);
}
finally {
DataPushingPacket.release(bytes);
}
}
/**
* Read a packet during DATA PUSHING phase.
*
* @return a {@link DataPushingPacket}
*
*/
private final DataPushingPacket readDataPushingPacket() throws IOException, NbdException {
final ClientConnection client = getConnection();
final ByteBuffer buffer = DataPushingPacket.allocateHeader();
try {
client.read(buffer);
return DataPushingPacket.deserialize(buffer);
}
finally {
DataPushingPacket.release(buffer);
}
}
}