package org.jscsi.target; import java.io.File; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.channels.AsynchronousCloseException; 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.Comparator; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.TreeMap; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import org.jscsi.parser.ProtocolDataUnit; import org.jscsi.target.connection.Connection.TargetConnection; import org.jscsi.target.connection.TargetSession; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * The central class of the jSCSI Target, which keeps track of all active {@link TargetSession}s, stores * target-wide parameters and variables, and * which contains the {@link #main(String[])} method for starting the program. * * @author Andreas Ergenzinger, University of Konstanz * @author Sebastian Graf, University of Konstanz */ public final class TargetServer implements Callable<Void> { private static final Logger LOGGER = LoggerFactory.getLogger(TargetServer.class); private static final TargetConnection[] EMPTY_TARGET_CONNECTION_ARRAY = new TargetConnection[0]; /** * A {@link SocketChannel} used for listening to incoming connections. */ private ServerSocketChannel serverSocketChannel; /** * A {@link Selector} used for listening to incoming connections and reading incoming datas */ // Guarded by this private Selector selector; /** * Mark the server as cancelled (atomic access to selector) */ // OODRIVE private AtomicBoolean cancelled = new AtomicBoolean(false); /** * Contains all active {@link TargetSession}s. */ private final List<TargetConnection> connections = new ArrayList<>(); /** * The jSCSI Target's global parameters. */ private final Configuration config; /** * */ // OODRIVE private DeviceIdentificationVpdPage deviceIdentificationVpdPage; /* * The table of targets, not case sensitive */ private static final Comparator<String> IGNORECASE_COMPARATOR = new Comparator<String>() { public final int compare(final String s1, final String s2) { return s1.compareToIgnoreCase(s2); } }; private final Map<String, Target> targets = new TreeMap<String, Target>(IGNORECASE_COMPARATOR); private final ReadWriteLock targetsLock = new ReentrantReadWriteLock(); /** * A target-wide counter used for providing the value of sent {@link ProtocolDataUnit}s' * <code>Target Transfer Tag</code> field, unless * that field is reserved. */ private static final AtomicInteger nextTargetTransferTag = new AtomicInteger(); /** * The connection the target server is using. */ // private Connection connection; //OODRIVE UNUSED public TargetServer(final Configuration conf) { this.config = conf; LOGGER.debug("Starting jSCSI-target: "); // read target settings from configuration file LOGGER.debug(" port: " + getConfig().getPort()); LOGGER.debug(" loading targets."); // open the storage medium targetsLock.writeLock().lock(); try{ List<Target> targetInfo = getConfig().getTargets(); for (Target curTargetInfo : targetInfo) { targets.put(curTargetInfo.getTargetName(), curTargetInfo); // print configuration and medium details LOGGER.debug(" target name: " + curTargetInfo.getTargetName() + " loaded."); } } finally {targetsLock.writeLock().unlock();} // OODRIVE this.deviceIdentificationVpdPage = new DeviceIdentificationVpdPage(this); } /** * Gets and increments the value to use in the next unreserved <code>Target Transfer Tag</code> field of * the next PDU to be sent by the * jSCSI Target. * * @see #nextTargetTransferTag * @return the value to use in the next unreserved <code>Target Transfer Tag * </code> field */ public static int getNextTargetTransferTag() { // value 0xffffffff is reserved int tag; do { tag = nextTargetTransferTag.getAndIncrement(); } while (tag == -1); return tag; } /** * Starts the jSCSI target. * * @param args * all command line arguments are ignored * @throws IOException */ public static void main(String[] args) throws Exception { TargetServer target; switch (args.length) { case 0: target = new TargetServer(Configuration.create()); break; case 1: target = new TargetServer(Configuration.create(Configuration.CONFIGURATION_SCHEMA_FILE, new File( args[0]))); break; case 2: target = new TargetServer(Configuration.create(new File(args[0]), new File(args[1]))); break; default: throw new IllegalArgumentException( "Only zero or one Parameter (Path to Configuration-File) allowed!"); } target.call(); } @Override public Void call() throws Exception { ExecutorService threadPool = Executors.newFixedThreadPool(4); // Create a blocking server socket and check for connections try { // Create a blocking server socket channel on the specified/default // port // cancelled: stop now if (cancelled.get()) { return null; } serverSocketChannel = ServerSocketChannel.open(); try { serverSocketChannel.configureBlocking(false); serverSocketChannel.socket().bind(new InetSocketAddress(getConfig().getTargetAddressInetAddress(), getConfig().getPort())); //OODRIVE synchronized (this) { if (cancelled.get()) { return null; } selector = Selector.open(); } try { serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); while (selector.select() >= 0 && !cancelled.get()) { final Iterator<SelectionKey> keys = selector.selectedKeys() .iterator(); while (keys.hasNext()) { SelectionKey key = keys.next(); keys.remove(); if (key.isValid()) { if (key.isAcceptable()) { // Accept the connection request. // If serverSocketChannel is blocking, this method blocks. // The returned channel is in blocking mode. final SocketChannel socketChannel = serverSocketChannel.accept(); TargetConnection connection = null ; try { // deactivate Nagle algorithm socketChannel.socket().setTcpNoDelay(true); socketChannel.socket().setKeepAlive(true); // non-blocking mode socketChannel.configureBlocking(false); // selection key can not be registered if a thread is blocked in the selector // so register it now with null interest set and update interest in another thread // when necessary SelectionKey selectionKey = socketChannel.register(selector, 0); // create a new connection for this client connection = new TargetConnection(socketChannel, true, selectionKey, this); synchronized (connections) { connections.add(connection); } threadPool.submit(connection.getPhase()); } catch(Exception e) { // Client connection not handled: must close socketChannel.close(); removeTargetConnection(connection); LOGGER.info("Throws Exception", e); continue; } } else{ try{ if (key.isReadable()) { SocketChannel clientSocketChannel = (SocketChannel) key.channel(); // Disable read selection int opsMask = key.interestOps(); opsMask &= ~(SelectionKey.OP_READ); key.interestOps(opsMask); // Handle request final TargetConnection connection = findConnection(clientSocketChannel); if (connection != null) { threadPool.submit(connection.getPhase()); } } }catch(final Exception e){ LOGGER.info("Throws Exception", e); continue; } } } } } } finally { selector.close(); synchronized (this) { selector = null; } } } finally { serverSocketChannel.close(); serverSocketChannel = null; } } catch (AsynchronousCloseException e) { // this block is entered if the server socket is close by cancel() LOGGER.debug("Throws Exception", e); } catch (IOException e) { // this block is entered if the desired port is already in use LOGGER.error("Throws Exception", e); } finally { threadPool.shutdownNow(); } return null; } // OODRIVE /** * Cancel the server if it is running. */ public void cancel() { final Selector selectorTmp; synchronized (this) { cancelled.set(true); selectorTmp = selector; } if (selectorTmp != null) { try { selectorTmp.wakeup(); } catch (Exception e) { // Already closed? LOGGER.debug("Throws Exception", e); } // Close the sessions - take a snapshot of the session list // to avoid a concurrent access during the close of the sessions final TargetConnection[] connectionsTmp; synchronized (connections) { connectionsTmp = connections.toArray(EMPTY_TARGET_CONNECTION_ARRAY); } for (int i = 0; i < connectionsTmp.length; i++) { connectionsTmp[i].close(); } } } // OODRIVE public List<TargetStats> getTargetStats() { // Get a snapshot of the current list of sessions targetsLock.readLock().lock(); try { final List<TargetStats> result = new ArrayList<>(targets.size()); for (Iterator<Target> iterator = targets.values().iterator(); iterator.hasNext();) { final Target target = iterator.next(); final String targetName = target.getTargetName(); // Look for sessions associated to that target final int count = getSessionsCount(targetName); final long size = target.getStorageModule().getSizeInBlocks() * target.getStorageModule().getBlockSize(); final TargetStats stats = new TargetStats(targetName, target.getTargetAlias(), count, size, target .getStorageModule().isWriteProtected()); result.add(stats); } return result; } finally { targetsLock.readLock().unlock(); } } // OODRIVE private int getSessionsCount(String targetName){ final TargetConnection[] connectionsTmp; synchronized (connections) { connectionsTmp = connections.toArray(EMPTY_TARGET_CONNECTION_ARRAY); } int count = 0; for (TargetConnection connection : connectionsTmp) { final TargetSession targetSession = connection.getTargetSession(); if (targetSession == null){ continue; } if (targetName.equalsIgnoreCase(targetSession.getTargetName())) { // one connection per session yet count++; } } return count; } // OODRIVE public boolean checkSessionParameters(TargetConnection newConnection, String targetName) { final TargetConnection[] connectionsTmp; final TargetSession newSession = newConnection.getTargetSession(); synchronized (connections) { connectionsTmp = connections.toArray(EMPTY_TARGET_CONNECTION_ARRAY); } for (TargetConnection targetConnection : connectionsTmp) { final TargetSession targetSession = targetConnection.getTargetSession(); if (targetSession == null) { continue; } if (targetName.equalsIgnoreCase(targetSession.getTargetName()) // same target name && newSession.getInitiatorSessionID().equals(targetSession.getInitiatorSessionID())) { // existing // ISID if (newSession.getInitiatorSessionHandle() == 0) { // TSIH zero // CID : any LOGGER.debug("Session reinstatement : disconnect old session"); targetConnection.close(); // disconnect the old socket return true; } else { // TSIH non-zero if (newSession.getInitiatorSessionHandle() == targetSession.getTargetSessionIdentifyingHandle()) { // existing // TSIH if (newConnection.getConnectionID() == targetConnection.getConnectionID()) { // existing CID LOGGER.debug("Connection reinstatement : disconnect old connection"); targetConnection.close(); // disconnect the old socket return true; } else { LOGGER.debug("Client is trying to add a new connection to an existing session : FAIL"); return false; } } else { // new TSIH // CID : any LOGGER.debug("Client is trying to use a none zero TSIH which does not correspond to the ISID : FAIL"); return false; } } } } // No session present, just check the Session handle if (newSession.getInitiatorSessionHandle() == 0) { // TSIH zero LOGGER.debug("Open a new session : OK"); return true; } else { // CID : any LOGGER.debug("Client is trying to use a non zero TSIH for a new session : FAIL"); return false; } } /** * Add a target. * * @param target target to add * @return previous target associated to that TargetName or null */ public Target addTarget(Target target) { final Target prev; targetsLock.writeLock().lock(); try { prev = targets.put(target.getTargetName(), target); } finally { targetsLock.writeLock().unlock(); } // print configuration and medium details if (prev == null) { LOGGER.debug("Target name: " + target.getTargetName() + " loaded"); } else { LOGGER.debug("Target name: " + target.getTargetName() + " reloaded"); } return prev; } public Target removeTarget(String targetName) { if (targetName == null) throw new NullPointerException(); final Target prev; targetsLock.writeLock().lock(); try { prev = targets.remove(targetName); } finally { targetsLock.writeLock().unlock(); } // print configuration and medium details if (prev == null) { // Nothing removed return null; } LOGGER.debug("Target name: " + targetName + " removed"); // Close sessions: get a snapshot of the session list (do // not lock the session list during the close of the sessions) final TargetConnection[] connectionsTmp; synchronized (connections) { connectionsTmp = connections.toArray(EMPTY_TARGET_CONNECTION_ARRAY); } for (int i = 0; i < connectionsTmp.length; i++) { final TargetConnection connection = connectionsTmp[i]; final TargetSession targetSession = connection.getTargetSession(); if (targetSession != null) { if (targetName.equalsIgnoreCase(targetSession.getTargetName())) { connection.close(); } } } return prev; } public Configuration getConfig() { return config; } /* OODRIVE public DeviceIdentificationVpdPage getDeviceIdentificationVpdPage() { return deviceIdentificationVpdPage; } */ public Target getTarget(String targetName) { targetsLock.readLock().lock(); try { return targets.get(targetName); } finally {targetsLock.readLock().unlock();} } /** * Removes a session from the jSCSI Target's list of active sessions. * * @param session * the session to remove from the list of active sessions */ public void removeTargetConnection(TargetConnection connection) { synchronized(connections) { connections.remove(connection); } } // OODRIVE private final TargetConnection findConnection(SocketChannel socket) { synchronized (connections) { for (int i = connections.size() - 1; i >= 0; i--) { final TargetConnection connection = connections.get(i); if (socket.equals(connection.getSocketChannel())) { return connection; } } // Not found return null; } } public String[] getTargetNames() { targetsLock.readLock().lock(); try { final String[] returnNames = new String[targets.size()]; return targets.keySet().toArray(returnNames); } finally {targetsLock.readLock().unlock();} } /** * Checks to see if this target name is valid. * * @param checkTargetName * @return true if the the target name is configured */ public boolean isValidTargetName(String checkTargetName) { targetsLock.readLock().lock(); try { return targets.containsKey(checkTargetName); } finally {targetsLock.readLock().unlock();} } /** * Using this connection mainly for test pruposes. * * @return the connection the target server established. */ // public Connection getConnection() { // return this.connection; // } /** * Gets the {@link TargetServer} selector. May be null. * * @return the selector */ public final Selector getSelector() { synchronized (this) { return selector; } } }