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.NbdServer.NbdConfiguration; 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.HashMap; 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.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Represents the server which contains the exports. * * @author oodrive * @author ebredzinski * @author llambert * */ final class ExportServer implements Callable<Void> { private static final Logger LOGGER = LoggerFactory.getLogger(ExportServer.class); private static final ClientConnection[] EMPTY_TARGET_CONNECTION_ARRAY = new ClientConnection[0]; /** Contains all the registered {@link NbdExport}s. */ private final Map<String, NbdExport> exports = new TreeMap<>(IGNORECASE_COMPARATOR); private final ReadWriteLock exportsLock = new ReentrantReadWriteLock(); private static final Comparator<String> IGNORECASE_COMPARATOR = new Comparator<String>() { @Override public final int compare(final String s1, final String s2) { return s1.compareToIgnoreCase(s2); } }; private static final String[] EMPTY_EXPORT_NAME_ARRAY = new String[0]; /** A {@link SocketChannel} used for listening to incoming connections. */ private ServerSocketChannel modernServerSocketChannel; /** A {@link Selector} used for listening to incoming connections and reading incoming datas */ private Selector selector; /** Mark the server as cancelled (atomic access to selector) */ private final AtomicBoolean cancelled = new AtomicBoolean(false); /** Contains all active {@link ClientConnection}s. */ private final Map<SocketChannel, ClientConnection> connections = new HashMap<>(); ExportServer(final NbdConfiguration conf) { this.config = conf; if (LOGGER.isDebugEnabled()) { LOGGER.debug("Starting NBD-export srver: "); LOGGER.debug(" port: " + getConfig().getPort()); LOGGER.debug(" loading exports."); } exportsLock.writeLock().lock(); try { final List<NbdExport> targetInfo = getConfig().getTargets(); for (final NbdExport curTargetInfo : targetInfo) { exports.put(curTargetInfo.getTargetName(), curTargetInfo); LOGGER.debug(" target name: " + curTargetInfo.getTargetName() + " loaded."); } } finally { exportsLock.writeLock().unlock(); } } /** * The NBD Export's global parameters. */ private final NbdConfiguration config; public final NbdConfiguration getConfig() { return config; } /** * Tells if trim is enabled or not. * * @return true if the trim is enabled, false otherwise */ public final boolean isTrimEnabled() { return config.isTrimEnabled(); } @Override public final Void call() throws Exception { final ExecutorService threadPool = Executors.newFixedThreadPool(4); try { modernServerSocketChannel = ServerSocketChannel.open(); try { modernServerSocketChannel.configureBlocking(false); final InetSocketAddress modernAddress = new InetSocketAddress( getConfig().getTargetAddressInetAddress(), getConfig().getPort()); modernServerSocketChannel.socket().bind(modernAddress); LOGGER.debug("Server started ..."); synchronized (this) { if (cancelled.get()) { return null; } selector = Selector.open(); } try { final SelectionKey acceptableKey = modernServerSocketChannel.register(selector, SelectionKey.OP_ACCEPT); while (selector.select() >= 0 && !cancelled.get()) { final Iterator<SelectionKey> keys = selector.selectedKeys().iterator(); while (keys.hasNext()) { final SelectionKey key = keys.next(); keys.remove(); // Event for a connection on the server socket if (key.isValid()) { if (key.isAcceptable()) { assert key == acceptableKey; LOGGER.debug("accept"); // Get the socket channel final ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel(); // Accept the client connection final SocketChannel clientSocketChannel = serverChannel.accept(); try { // Configure the new client socket final SocketHandle socketHandle = new SocketHandle(clientSocketChannel, selector); socketHandle.configure(); if (LOGGER.isDebugEnabled()) { LOGGER.debug("connection from a client, new socket : " + clientSocketChannel.socket()); } // Create a new client connection final ClientConnection connection = new ClientConnection(socketHandle, this, serverChannel.equals(modernServerSocketChannel)); // Save the connection addConnection(clientSocketChannel, connection); // Execute first phase threadPool.submit(connection.getPhase()); } catch (final Exception e) { LOGGER.warn("Unexpected exception", e); removeConnection(clientSocketChannel); clientSocketChannel.close(); continue; } } // event for the reception on a client socket else { try { final SocketChannel clientSocketChannel = (SocketChannel) key.channel(); // Get the corresponding connection and execute the current phase final ClientConnection connection = getConnection(clientSocketChannel); if (connection != null) { if (connection.isReadable()) { threadPool.submit(connection.getPhase()); } } else { LOGGER.warn("Connection can not be retrieved: " + clientSocketChannel.socket()); clientSocketChannel.close(); continue; } } catch (final Exception e) { LOGGER.warn("Unexpected exception", e); continue; } } } } } } finally { selector.close(); synchronized (this) { selector = null; } } } finally { modernServerSocketChannel.close(); } } catch (final AsynchronousCloseException e) { // this block is entered if the server socket is close by cancel() LOGGER.debug("Throws Exception", e); } catch (final IOException e) { // this block is entered if the desired port is already in use LOGGER.error("Throws Exception", e); } finally { threadPool.shutdownNow(); } return null; } /** * Add a connection. * * @param socketChannel * the {@link SocketChannel} corresponding to the connection * * @param connection * the {@link ClientConnection} * */ private final void addConnection(final SocketChannel socketChannel, final ClientConnection connection) { synchronized (connections) { connections.put(socketChannel, connection); } } /** * Gets a connection. * * @param socketChannel * the {@link SocketChannel} corresponding to the connection * */ private final ClientConnection getConnection(final SocketChannel socketChannel) { synchronized (connections) { return connections.get(socketChannel); } } /** * Remove a connection. * * @param socketChannel * the {@link SocketChannel} corresponding to the connection * */ final void removeConnection(final SocketChannel socketChannel) { synchronized (connections) { connections.remove(socketChannel); } } /** * Cancel the server. * */ final void cancel() { final Selector selectorTmp; LOGGER.debug("Cancel server"); synchronized (this) { cancelled.set(true); selectorTmp = selector; } if (selectorTmp != null) { try { selectorTmp.wakeup(); } catch (final 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 ClientConnection[] connectionsTmp; synchronized (connections) { connectionsTmp = connections.values().toArray(EMPTY_TARGET_CONNECTION_ARRAY); } for (int i = 0; i < connectionsTmp.length; i++) { connectionsTmp[i].close(); } } } /** * Add an export. * * @param export * the {@link NbdExport} to add. * @return the previous export- May be null * * @throws IOException * */ final NbdExport addExport(final NbdExport export) { exportsLock.writeLock().lock(); try { return exports.put(export.getTargetName(), export); } finally { exportsLock.writeLock().unlock(); } } /** * Remove an export. * * @param exportName * the name of the {@link NbdExport} to remove * @throws IOException * */ final NbdExport removeExport(final String exportName) { final NbdExport prev; exportsLock.writeLock().lock(); try { prev = exports.remove(exportName); } finally { exportsLock.writeLock().unlock(); } if (prev == null) { // Nothing removed return null; } LOGGER.debug("Target name: " + exportName + " removed"); // Close sessions: get a snapshot of the session list (do // not lock the connection list during the close of the connections) final ClientConnection[] connectionsTmp; synchronized (connections) { connectionsTmp = connections.values().toArray(EMPTY_TARGET_CONNECTION_ARRAY); } for (int i = 0; i < connectionsTmp.length; i++) { final ClientConnection connection = connectionsTmp[i]; final String connectionExportName = connection.getExportName(); if (exportName.equalsIgnoreCase(connectionExportName)) { connection.close(); } } return prev; } /** * Gets the current export list. * * @return an array of export name * */ final String[] getExportList() { exportsLock.readLock().lock(); try { return exports.keySet().toArray(EMPTY_EXPORT_NAME_ARRAY); } finally { exportsLock.readLock().unlock(); } } /** * Gets the device corresponding to a given name. * * @param exportName * the name of the export. * */ final NbdDevice getDevice(final String exportName) { NbdExport export; exportsLock.readLock().lock(); try { export = exports.get(exportName); if (export == null) { throw new IllegalArgumentException("Unknown export name '" + exportName + "'"); } return export.getDevice(); } finally { exportsLock.readLock().unlock(); } } /** * Get a snapshot of the current list of sessions. * * @return the list of the exports */ public final List<NbdExportStats> getTargetStats() { // exportsLock.readLock().lock(); try { final List<NbdExportStats> result = new ArrayList<>(exports.size()); for (final Iterator<NbdExport> iterator = exports.values().iterator(); iterator.hasNext();) { final NbdExport target = iterator.next(); final String targetName = target.getTargetName(); // Look for connections associated to that target final int count = getConnectionsCount(targetName); final NbdExportStats stats = new NbdExportStats(targetName, count, target.getSize(), target.isReadOnly()); result.add(stats); } return result; } finally { exportsLock.readLock().unlock(); } } /** * Gets the number of connection on a target. * * @param targetName * the target name * @return the number of connection */ private final int getConnectionsCount(final String targetName) { final ClientConnection[] connectionsTmp; synchronized (connections) { connectionsTmp = connections.values().toArray(EMPTY_TARGET_CONNECTION_ARRAY); } int count = 0; for (final ClientConnection connection : connectionsTmp) { final String exportName = connection.getExportName(); if (exportName == null) { continue; } if (targetName.equalsIgnoreCase(exportName)) { count++; } } return count; } }