/**
* Copyright 2013-2015 Seagate Technology LLC.
*
* This Source Code Form is subject to the terms of the Mozilla
* Public License, v. 2.0. If a copy of the MPL was not
* distributed with this file, You can obtain one at
* https://mozilla.org/MP:/2.0/.
*
* This program is distributed in the hope that it will be useful,
* but is provided AS-IS, WITHOUT ANY WARRANTY; including without
* the implied warranty of MERCHANTABILITY, NON-INFRINGEMENT or
* FITNESS FOR A PARTICULAR PURPOSE. See the Mozilla Public
* License for more details.
*
* See www.openkinetic.org for more project information
*/
package com.seagate.kinetic.simulator.internal;
import io.netty.channel.ChannelHandlerContext;
import java.io.File;
import java.net.UnknownHostException;
import java.security.Key;
import java.util.ArrayList;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import kinetic.simulator.SimulatorConfiguration;
import com.seagate.kinetic.common.lib.KineticMessage;
import com.seagate.kinetic.heartbeat.message.ByteCounter;
import com.seagate.kinetic.heartbeat.message.OperationCounter;
import com.seagate.kinetic.proto.Kinetic.Command;
import com.seagate.kinetic.proto.Kinetic.Command.GetLog.Configuration;
import com.seagate.kinetic.proto.Kinetic.Command.GetLog.Limits;
import com.seagate.kinetic.proto.Kinetic.Command.MessageType;
import com.seagate.kinetic.proto.Kinetic.Command.Security.ACL;
import com.seagate.kinetic.proto.Kinetic.Command.Status.StatusCode;
import com.seagate.kinetic.proto.Kinetic.Local;
import com.seagate.kinetic.proto.Kinetic.Message;
import com.seagate.kinetic.proto.Kinetic.Message.AuthType;
import com.seagate.kinetic.simulator.heartbeat.Heartbeat;
import com.seagate.kinetic.simulator.internal.handler.CommandManager;
import com.seagate.kinetic.simulator.io.provider.nio.NioEventLoopGroupManager;
import com.seagate.kinetic.simulator.io.provider.spi.MessageService;
import com.seagate.kinetic.simulator.io.provider.spi.TransportProvider;
import com.seagate.kinetic.simulator.persist.Store;
import com.seagate.kinetic.simulator.persist.StoreFactory;
import com.seagate.kinetic.simulator.utility.ConfigurationUtil;
import com.seagate.kinetic.simulator.utility.LimitsUtil;
/**
*
* Simulator boot-strap class.
* <p>
* Applications use this class to start new instance(s) of the simulator.
* <p>
* There is a main method provided in this class as a reference as well as a
* default simulator behavior. Applications may also define their own
* SimulatorConfigration instances and start the simulator with customized
* configurations.
* <p>
* Applications may plug-in persistent store for the simulator based on the
* contract defined in the Store interface.
* <p>
* Applications may also plug-in transport for the simulator based on the
* contract defined on the TransportProvider interface.
* <p>
*
* @see SimulatorConfiguration
* @see StoreFactory.
* @see Store
* @see MessageService
*
* @author James Hughes
* @author Chiaming Yang
*/
public class SimulatorEngine implements MessageService {
private final static Logger logger = Logger.getLogger(SimulatorEngine.class
.getName());
// protocol version
public static final String PROTOCOL_VERSION = Local.getDefaultInstance()
.getProtocolVersion();
private SimulatorConfiguration config = null;
@SuppressWarnings("rawtypes")
private Store store = null;
private final ArrayList<TransportProvider> transports = new ArrayList<TransportProvider>();
private final TransportProvider sslService = null;
// ack map
private Map<Long, ACL> aclmap = null;
private SecurityPin securityPin = new SecurityPin();
private Map<Long, Key> hmacKeyMap = null;
private long clusterVersion = 0;
private final boolean isHttp = Boolean.getBoolean("kinetic.io.http");
private final boolean isHttps = Boolean.getBoolean("kinetic.io.https");
private final boolean isUdt = Boolean.getBoolean("kinetic.io.udt");
// define this to load the user defined transport provider
private final boolean isLoadTransportPlugIn = Boolean
.getBoolean("kinetic.io.plugin");
// if isLoadTransportPlugIn is defined to true, specified the transport
// provider class name with the following property.
private final String TRANSPORT_PLUGIN_CLASS = "kinetic.io.plugin.class";
private static final String UDT_TRANSPORT = "com.seagate.kinetic.simulator.io.provider.nio.udt.UdtTransportProvider";
private static final String HTTP_TRANSPORT = "com.seagate.kinetic.simulator.io.provider.nio.http.HttpTransportProvider";
private static final String TCP_TRANSPORT = "com.seagate.kinetic.simulator.io.provider.tcp.TcpTransportProvider";
private static final String TCP_NIO_TRANSPORT = "com.seagate.kinetic.simulator.io.provider.nio.tcp.TcpNioTransportProvider";
private static final String SSL_NIO_TRANSPORT = "com.seagate.kinetic.simulator.io.provider.nio.ssl.SslNioTransportProvider";
// private P2POperationHandler p2pHandler = null;
// batch op handler
private BatchOperationHandler batchOp = null;
private NioEventLoopGroupManager nioManager = null;
private Heartbeat heartbeat = null;
// operation counter
private final OperationCounter operationCounter = new OperationCounter();
// byte counter
private final ByteCounter byteCounter = new ByteCounter();
// flag to indicate if the simulator is closing
private volatile boolean isClosing = false;
private String kineticHome = null;
// resource for all the simulator instances
private static ThreadPoolService tpService = new ThreadPoolService();
// shutdown hook
private static SimulatorShutdownHook shutdownHook = new SimulatorShutdownHook(
tpService);
// connection map
private static ConcurrentHashMap<Object, ConnectionInfo> connectionMap = new ConcurrentHashMap<Object, ConnectionInfo>();
// last connection Id.
private static long lastConnectionId = System.currentTimeMillis();
private volatile boolean deviceLocked = false;
static {
// add shutdown hook to clean up resources
Runtime.getRuntime().addShutdownHook(shutdownHook);
}
private CommandManager manager = null;
/**
* Simulator constructor.
*
* @param config
* simulator configuration.
*/
public SimulatorEngine(SimulatorConfiguration config) {
// config for the current instance
this.config = config;
try {
// calculate my home
kineticHome = kineticHome(config);
// heart beat
if (config.getTickTime() > 0) {
// construct new heart beat instance
this.heartbeat = new Heartbeat(this);
}
// register to use thread pool
tpService.register(this);
// load acl and pins
SecurityHandler.loadACL(this);
// load set up
SetupHandler.loadSetup(this);
// initialize db store
this.initStore();
// init network io service
this.initIoService();
// init op handlers
this.initHandlers();
logger.info("simulator protocol version = "
+ SimulatorConfiguration.getProtocolVersion() + ", wwn="
+ config.getWorldWideName());
} catch (Exception e) {
e.printStackTrace();
close();
}
}
/**
* Load, initialize, and start simulator transport services.
*
* @throws Exception
* if any exception occurred.
*/
private void initIoService() throws Exception {
/**
* load transports
*/
this.loadTransports();
/**
* start transports
*/
for (TransportProvider transport : transports) {
transport.init(this);
transport.start();
}
}
/**
* Load transport providers with simple rules.
* <p>
* The default is to load tcpnio and sslnio transport providers. These are
* supported transports and are compatible with the Kinetic drive protocol.
* <p>
* The rest of the transport are experimental and are not
* compatible/supported by Kinetic drive.
* <p>
* If users specifies to load from a plug-in class, then it is loaded and
* used.
* <p>
* else the loading logic is checked as defined in-lines.
*/
private void loadTransports() {
// if "kinetic.io.plugin" property is defined, then the class of the
// provider is loaded.
if (isLoadTransportPlugIn) {
// get the class name from the property "kinetic.io.plugin.class"
String plugInName = config.getProperty(TRANSPORT_PLUGIN_CLASS);
logger.info("loading plugin transport, class name=" + plugInName);
this.loadProvider(plugInName);
} else if (this.isUdt) {
// if "kinetic,io.udt" is set to true, load UDT transport
this.loadProvider(UDT_TRANSPORT);
} else if (isHttp) {
// if "kinetic.io.http" is set to true, load http transport
this.loadProvider(HTTP_TRANSPORT);
} else if (isHttps) {
// if "kinetic.io.https" is set to true, load http transport with
// ssl
// filter
this.loadProvider(HTTP_TRANSPORT);
} else if (this.config.getUseNio()) {
// if "set ssl as default" flag is set, load ssl transport as
// "default" service
if (this.config.getUseSslAsDefault()) {
this.loadProvider(SSL_NIO_TRANSPORT);
} else {
// load tcpnio transport as "default" service
this.loadProvider(TCP_NIO_TRANSPORT);
}
} else {
// load tcp transport
this.loadProvider(TCP_TRANSPORT);
}
// load ssl transport, default is set to true.
if (this.config.getStartSsl()) {
this.loadProvider(SSL_NIO_TRANSPORT);
}
}
private void initHandlers() {
this.manager = new CommandManager(this);
this.batchOp = new BatchOperationHandler(this);
}
public CommandManager getCommandManager() {
return this.manager;
}
public boolean useNio() {
return this.config.getUseNio();
}
public boolean isStartSsl() {
return (this.sslService != null);
}
public Map<Long, ACL> getAclMap() {
return this.aclmap;
}
public void setAclMap(Map<Long, ACL> aclmap) {
this.aclmap = aclmap;
}
public void setHmacKeyMap(Map<Long, Key> hmacKeyMap) {
this.hmacKeyMap = hmacKeyMap;
}
public Map<Long, Key> getHmacKeyMap() {
return this.hmacKeyMap;
}
@SuppressWarnings("rawtypes")
public Store getStore() {
return this.store;
}
public void setClusterVersion(long cversion) {
this.clusterVersion = cversion;
}
public long getClusterVersion() {
return this.clusterVersion;
}
public SecurityPin getSecurityPin() {
return this.securityPin;
}
public String getKineticHome() {
return this.kineticHome;
}
/**
* start new instance of store.
*/
private void initStore() {
this.store = StoreFactory.createInstance(this.config);
}
/**
* Get server configuration.
*
* @return server configuration.
*/
@Override
public SimulatorConfiguration getServiceConfiguration() {
return this.config;
}
public void close() {
if (this.isClosing) {
return;
}
this.isClosing = true;
tpService.deregister(this);
// close handlers
if (this.manager != null) {
this.manager.close();
}
// close io resources
if (this.nioManager != null) {
this.nioManager.close();
}
// close transport providers
this.closeTransportServices();
// close db store
if (this.store != null) {
this.store.close();
}
}
/**
* close transport provider and release associated resources.
*/
private void closeTransportServices() {
for (TransportProvider transport : transports) {
try {
transport.stop();
transport.close();
} catch (Exception e) {
logger.log(Level.WARNING, e.getMessage(), e);
}
}
}
public static void logBytes(String name, byte[] b) {
final int MAX_LENGTH = 50; // only log up to MAX_LENGTH bytes
int length = b.length;
if (length > MAX_LENGTH) {
length = MAX_LENGTH;
}
StringBuffer sb = new StringBuffer(name + ": ");
for (int i = 0; i < length; i++) {
sb.append(String.format("%02x ", b[i]));
}
logger.fine(sb.toString());
}
public static String kineticHome(SimulatorConfiguration config) {
String kineticHome = config.getSimulatorHome();
File lchome = new File(kineticHome);
if (!lchome.exists()) {
lchome.mkdirs();
}
return kineticHome;
}
@Override
public KineticMessage processRequest(KineticMessage kmreq) {
// create request context
RequestContext context = new RequestContext(this, kmreq);
try {
// prepare to process this request
context.preProcessRequest();
// check if in batch mode
this.batchOp.checkBatchMode(kmreq);
if (kmreq.getIsBatchMessage()) {
this.batchOp.handleRequest(context);
} else {
// process request
context.processRequest();
}
} catch (Exception e) {
logger.log(Level.WARNING, e.getMessage(), e);
/**
* reset to default error response code if not set
*/
if (context.getCommandBuilder().getStatusBuilder().getCode() == StatusCode.SUCCESS) {
context.getCommandBuilder().getStatusBuilder()
.setCode(
StatusCode.INVALID_REQUEST);
}
// set status message
context.getCommandBuilder()
.getStatusBuilder()
.setStatusMessage(
e.getClass().getName() + ":" + e.getMessage());
} finally {
try {
// post process message
context.postProcessRequest();
} catch (Exception e2) {
logger.log(Level.WARNING, e2.getMessage(), e2);
}
this.addStatisticCounter(kmreq, context.getResponseMessage());
}
return context.getResponseMessage();
}
private void addStatisticCounter(KineticMessage kmreq, KineticMessage kmresp) {
try {
Message request = (Message) kmreq.getMessage();
Message response = ((Message.Builder) kmresp.getMessage()).build();
MessageType mtype = kmreq.getCommand().getHeader().getMessageType();
int inCount = 0;
int outCount = 0;
if (request != null) {
inCount = request.getSerializedSize();
// add in-bound value byte count
if (kmreq.getValue() != null) {
inCount = inCount + kmreq.getValue().length;
}
}
if (response != null) {
outCount = response.getSerializedSize();
// add out-bound value byte count
if (kmresp.getValue() != null) {
outCount = outCount + kmresp.getValue().length;
}
}
switch (mtype) {
case GET:
this.operationCounter.addGetCounter();
this.byteCounter.addGetCounter(inCount);
this.byteCounter.addGetCounter(outCount);
break;
case PUT:
this.operationCounter.addPutCounter();
if (request != null) {
this.byteCounter.addPutCounter(inCount);
}
if (response != null) {
this.byteCounter.addPutCounter(outCount);
}
break;
case DELETE:
this.operationCounter.addDeleteCounter();
if (request != null) {
this.byteCounter.addDeleteCounter(inCount);
}
if (response != null) {
this.byteCounter.addDeleteCounter(outCount);
}
break;
case GETNEXT:
this.operationCounter.addGetNextCounter();
if (request != null) {
this.byteCounter.addGetNextCounter(inCount);
}
if (response != null) {
this.byteCounter.addGetNextCounter(outCount);
}
break;
case GETPREVIOUS:
this.operationCounter.addGetPreviousCounter();
if (request != null) {
this.byteCounter.addGetPreviousCounter(inCount);
}
if (response != null) {
this.byteCounter.addGetPreviousCounter(outCount);
}
break;
case GETKEYRANGE:
this.operationCounter.addGetKeyRangeCounter();
if (request != null) {
this.byteCounter.addGetKeyRangeCounter(inCount);
}
if (response != null) {
this.byteCounter.addGetKeyRangeCounter(outCount);
}
break;
case GETVERSION:
this.operationCounter.addGetVersionCounter();
if (request != null) {
this.byteCounter.addGetVersionCounter(inCount);
}
if (response != null) {
this.byteCounter.addGetVersionCounter(outCount);
}
break;
case SETUP:
this.operationCounter.addSetupCounter();
if (request != null) {
this.byteCounter.addSetupCounter(inCount);
}
if (response != null) {
this.byteCounter.addSetupCounter(outCount);
}
break;
case GETLOG:
this.operationCounter.addGetLogCounter();
if (request != null) {
this.byteCounter.addGetLogCounter(inCount);
}
if (response != null) {
this.byteCounter.addGetLogCounter(outCount);
}
break;
case SECURITY:
this.operationCounter.addSecurityCounter();
if (request != null) {
this.byteCounter.addSecurityCounter(inCount);
}
if (response != null) {
this.byteCounter.addSecurityCounter(outCount);
}
break;
case PEER2PEERPUSH:
this.operationCounter.addP2PCounter();
if (request != null) {
this.byteCounter.addP2PCounter(inCount);
}
if (response != null) {
this.byteCounter.addP2PCounter(outCount);
}
break;
default:
break;
}
} catch (Exception e) {
logger.log(Level.WARNING, e.getMessage(), e);
}
}
@Override
public synchronized NioEventLoopGroupManager getNioEventLoopGroupManager() {
if (this.nioManager == null) {
this.nioManager = new NioEventLoopGroupManager(this.config);
}
return this.nioManager;
}
@Override
public void execute(Runnable request) {
tpService.execute(request);
}
public Heartbeat getHearBeat() {
return this.heartbeat;
}
public ByteCounter getByteCounter() {
return this.byteCounter;
}
public OperationCounter getOperationCounter() {
return this.operationCounter;
}
/**
* load transport provider with the specified class name.
*
* @param className
* the provider class full name to be loaded.
*/
private void loadProvider(String className) {
TransportProvider provider = null;
try {
// load class
provider = (TransportProvider) Class.forName(className)
.newInstance();
// add to transport list
this.transports.add(provider);
logger.info("transport provider added., class name=" + className);
} catch (Exception e) {
logger.log(Level.WARNING, e.getMessage(), e);
}
}
/**
* put connection/connection info into the connection map.
*
* @param connection
* the key for the entry
* @param cinfo
* value of the entry
* @return the previous value associated with key, or null if there was no
* mapping for key
*/
public static ConnectionInfo putConnectionInfo(Object connection,
ConnectionInfo cinfo) {
return connectionMap.put(connection, cinfo);
}
/**
* Get connection info based on the specified key.
*
* @param connection
* key to get the connection info.
*
* @return the value to which the specified key is mapped, or null if this
* map contains no mapping for the key
*/
public static ConnectionInfo getConnectionInfo(Object connection) {
return connectionMap.get(connection);
}
/**
* remove the value of the specified key.
*
* @param connection
* the key od the entry that needs to be removed
* @return the previous value associated with key, or null if there was no
* mapping for key
*/
public static ConnectionInfo removeConnectionInfo(Object connection) {
return connectionMap.remove(connection);
}
/**
* register a new connection. A new connection info instance is created and
* associated with the connection.
*
* @param connection
* the new connection to be added to the connection map.
*
* @return the connection info instance associated with the connection.
*/
@Override
public ConnectionInfo registerNewConnection(ChannelHandlerContext ctx) {
ConnectionInfo info = newConnectionInfo();
putConnectionInfo(ctx, info);
KineticMessage km = new KineticMessage();
Message.Builder mb = Message.newBuilder();
mb.setAuthType(AuthType.UNSOLICITEDSTATUS);
Command.Builder cb = Command.newBuilder();
// connection id
cb.getHeaderBuilder().setConnectionID(info.getConnectionId());
// cluster version
cb.getHeaderBuilder().setClusterVersion(this.clusterVersion);
// configurations
try {
Configuration configuration = ConfigurationUtil
.getConfiguration(this);
cb.getBodyBuilder().getGetLogBuilder().getConfigurationBuilder()
.mergeFrom(configuration);
} catch (Exception e) {
logger.log(Level.WARNING, e.getMessage(), e);
}
// limits
try {
Limits limits = LimitsUtil.getLimits(this.config);
cb.getBodyBuilder().getGetLogBuilder().getLimitsBuilder()
.mergeFrom(limits);
} catch (UnknownHostException e) {
logger.log(Level.WARNING, e.getMessage(), e);
}
// status
cb.getStatusBuilder().setCode(StatusCode.SUCCESS);
mb.setCommandBytes(cb.build().toByteString());
km.setMessage(mb);
km.setCommand(cb);
ctx.writeAndFlush(km);
// logger.info("***** connection registered., sent UNSOLICITEDSTATUS with cid = "
// + info.getConnectionId());
return info;
}
/**
* instantiate a new connection info object with connection id set.
*
* @return a new connection info object with connection id set
*/
public static ConnectionInfo newConnectionInfo() {
ConnectionInfo info = new ConnectionInfo();
info.setConnectionId(getNextConnectionId());
return info;
}
/**
* Get next available unique connection id based on timestamp. The Id is
* guarantees to be unique for simulators running within the same JVM.
*
* @return next available unique connection ID based on timestamp.
*/
private static synchronized long getNextConnectionId() {
// current time
long id = System.currentTimeMillis();
// check if duplicate. enforce so that it is later than the time that
// this JVM is started.
if (id <= lastConnectionId) {
// increase one so its unique.
id = lastConnectionId + 1;
}
// set last connection id
lastConnectionId = id;
return id;
}
/**
* create an internal message with empty builder message.
*
* @return an internal message with empty builder message
*/
public static KineticMessage createKineticMessageWithBuilder() {
// new instance of internal message
KineticMessage kineticMessage = new KineticMessage();
// new builder message
Message.Builder message = Message.newBuilder();
// set to im
kineticMessage.setMessage(message);
// set hmac auth type
message.setAuthType(AuthType.HMACAUTH);
// create command builder
Command.Builder commandBuilder = Command.newBuilder();
// set command
kineticMessage.setCommand(commandBuilder);
return kineticMessage;
}
/**
* Lock/unlock the device/simulator
*
* @param flag
*/
public void setDeviceLocked(boolean flag) {
this.deviceLocked = flag;
}
/**
* Get device lock flag.
*
* @return true if locked. Otherwise, return false.
*/
public boolean getDeviceLocked() {
return this.deviceLocked;
}
/**
* Get the absolute path of the persist store.
*
* @return the absolute path of the persist store
*
*/
public String getPersistStorePath() {
String path = "/";
try {
path = this.store.getPersistStorePath();
} catch (KVStoreException e) {
logger.log(Level.WARNING, e.getMessage(), e);
}
return path;
}
}