package com.intrbiz.bergamot.agent; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.net.URI; import java.security.KeyStore; import java.security.SecureRandom; import java.util.Timer; import java.util.TimerTask; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLParameters; import javax.net.ssl.TrustManagerFactory; import javax.xml.bind.JAXBException; import org.apache.log4j.BasicConfigurator; import org.apache.log4j.Level; import org.apache.log4j.Logger; import org.apache.log4j.PropertyConfigurator; import org.hyperic.sigar.Humidor; import org.hyperic.sigar.SigarException; import com.intrbiz.bergamot.agent.config.BergamotAgentCfg; import com.intrbiz.bergamot.agent.config.Configurable; import com.intrbiz.bergamot.agent.handler.AgentInfoHandler; import com.intrbiz.bergamot.agent.handler.AgentRegistrationHandler; import com.intrbiz.bergamot.agent.handler.CPUInfoHandler; import com.intrbiz.bergamot.agent.handler.DefaultHandler; import com.intrbiz.bergamot.agent.handler.DiskIOHandler; import com.intrbiz.bergamot.agent.handler.DiskInfoHandler; import com.intrbiz.bergamot.agent.handler.ExecHandler; import com.intrbiz.bergamot.agent.handler.MemInfoHandler; import com.intrbiz.bergamot.agent.handler.NetConInfoHandler; import com.intrbiz.bergamot.agent.handler.NetIOHandler; import com.intrbiz.bergamot.agent.handler.NetIfInfoHandler; import com.intrbiz.bergamot.agent.handler.OSInfoHandler; import com.intrbiz.bergamot.agent.handler.ProcessInfoHandler; import com.intrbiz.bergamot.agent.handler.UptimeInfoHandler; import com.intrbiz.bergamot.agent.handler.WhoInfoHandler; import com.intrbiz.bergamot.model.message.agent.AgentMessage; import com.intrbiz.bergamot.model.message.agent.error.AgentError; import com.intrbiz.bergamot.model.message.agent.ping.AgentPing; import com.intrbiz.bergamot.model.message.agent.ping.AgentPong; import io.netty.bootstrap.Bootstrap; import io.netty.buffer.UnpooledByteBufAllocator; import io.netty.channel.Channel; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelOption; import io.netty.channel.ChannelPipeline; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.handler.codec.http.HttpClientCodec; import io.netty.handler.codec.http.HttpObjectAggregator; import io.netty.handler.ssl.SslHandler; import io.netty.handler.timeout.ReadTimeoutHandler; import io.netty.handler.timeout.WriteTimeoutHandler; import io.netty.util.concurrent.GenericFutureListener; /** */ public class BergamotAgent implements Configurable<BergamotAgentCfg> { public static final String AGENT_VENDOR = "Bergamot Monitoring"; public static final String AGENT_PRODUCT = "Bergamot Agent"; public static final String AGENT_VERSION = "3.0.0"; private static final Logger logger = Logger.getLogger(BergamotAgent.class); private URI server; private EventLoopGroup eventLoop; private Timer timer; private ConcurrentMap<Class<?>, AgentHandler> handlers = new ConcurrentHashMap<Class<?>, AgentHandler>(); private AgentHandler defaultHandler; private SSLContext sslContext; private BergamotAgentCfg configuration; private volatile Channel channel; private AtomicInteger connectionAttempt = new AtomicInteger(0); public BergamotAgent() { super(); this.timer = new Timer(); // background GC task // we want to deliberately // keep memory to a minimum this.timer.scheduleAtFixedRate(new TimerTask() { @Override public void run() { System.gc(); Runtime rt = Runtime.getRuntime(); logger.debug("Memory, free: " + rt.freeMemory() + " total: " + rt.totalMemory() + " max: " + rt.maxMemory()); } }, 30000L, 30000L); // handlers this.setDefaultHandler(new DefaultHandler()); this.registerHandler(new AgentRegistrationHandler()); this.registerHandler(new CPUInfoHandler()); this.registerHandler(new MemInfoHandler()); this.registerHandler(new DiskInfoHandler()); this.registerHandler(new OSInfoHandler()); this.registerHandler(new UptimeInfoHandler()); this.registerHandler(new NetIfInfoHandler()); this.registerHandler(new ExecHandler()); this.registerHandler(new ProcessInfoHandler()); this.registerHandler(new WhoInfoHandler()); this.registerHandler(new NetConInfoHandler()); this.registerHandler(new AgentInfoHandler()); this.registerHandler(new NetIOHandler()); this.registerHandler(new DiskIOHandler()); // shutdown hook Runtime.getRuntime().addShutdownHook(new Thread() { @Override public void run() { BergamotAgent.logger.info("Bergamot Agent shutting down"); if (BergamotAgent.this.channel != null && BergamotAgent.this.channel.isActive()) { BergamotAgent.this.channel.close(); } } }); } public BergamotAgentCfg getConfiguration() { return this.configuration; } @Override public void configure(BergamotAgentCfg cfg) throws Exception { this.configuration = cfg; // configure this agent this.server = new URI(cfg.getServer()); logger.info("Bergamot Agent, connecting to " + this.server + " configured"); this.eventLoop = new NioEventLoopGroup(1); this.sslContext = this.createContext(); } private SSLContext createContext() { try { String pass = "abc123"; // create the keystore KeyStore sks = KeyStoreUtil.loadClientAuthKeyStore(pass, this.configuration.getKeyTrimmed(), this.configuration.getCertificateTrimmed(), this.configuration.getSiteCaCertificateTrimmed(), this.configuration.getCaCertificateTrimmed()); KeyStore tks = KeyStoreUtil.loadTrustKeyStore(this.configuration.getCaCertificateTrimmed()); // the key manager KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); kmf.init(sks, pass.toCharArray()); // the trust manager TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); tmf.init(tks); // the context SSLContext context = SSLContext.getInstance("TLS"); context.init(kmf.getKeyManagers(), tmf.getTrustManagers(), new SecureRandom()); return context; } catch (Exception e) { throw new RuntimeException("Failed to init SSLContext", e); } } private SSLEngine createSSLEngine(String host, int port) { try { SSLEngine sslEngine = this.sslContext.createSSLEngine(host, port); sslEngine.setUseClientMode(true); sslEngine.setNeedClientAuth(true); // set TLS protocols sslEngine.setEnabledProtocols(TLSUtils.computeSupportedProtocols(sslEngine, TLSUtils.PROTOCOLS.SAFE_PROTOCOLS)); SSLParameters params = new SSLParameters(); // can't do this in JDK 6 // params.setEndpointIdentificationAlgorithm("HTTPS"); params.setNeedClientAuth(true); sslEngine.setSSLParameters(params); return sslEngine; } catch (Exception e) { throw new RuntimeException("Failed to init SSLEngine", e); } } public URI getServer() { return this.server; } public void registerHandler(AgentHandler handler) { for (Class<?> cls : handler.getMessages()) { this.handlers.put(cls, handler); handler.init(this); } } public void setDefaultHandler(AgentHandler handler) { this.defaultHandler = handler; if (this.defaultHandler != null) this.defaultHandler.init(this); } public AgentHandler getHandler(Class<?> messageType) { AgentHandler handler = this.handlers.get(messageType); return handler == null ? this.defaultHandler : handler; } public void start() { // init Sigar early on try { logger.info("Bergamot Agent starting on: " + Humidor.getInstance().getSigar().getFQDN()); } catch (SigarException e) { logger.error("Failed to setup Sigar!", e); throw new RuntimeException("Failed to setup Sigar, aborting!", e); } // start the connection process this.connect(); } private void connect() { final SSLEngine engine = createSSLEngine(this.server.getHost(), this.server.getPort()); // configure the client Bootstrap b = new Bootstrap(); b.group(this.eventLoop); b.channel(NioSocketChannel.class); b.option(ChannelOption.ALLOCATOR, new UnpooledByteBufAllocator(false)); b.handler(new ChannelInitializer<SocketChannel>() { @Override public void initChannel(SocketChannel ch) throws Exception { // HTTP handling ChannelPipeline pipeline = ch.pipeline(); pipeline.addLast("read-timeout", new ReadTimeoutHandler( 90 /* seconds */ )); pipeline.addLast("write-timeout", new WriteTimeoutHandler( 90 /* seconds */ )); pipeline.addLast("ssl", new SslHandler(engine)); pipeline.addLast("codec", new HttpClientCodec()); pipeline.addLast("aggregator", new HttpObjectAggregator(65536)); pipeline.addLast("handler", new AgentClientHandler(BergamotAgent.this.timer, BergamotAgent.this.server) { @Override protected AgentMessage processAgentMessage(final ChannelHandlerContext ctx, final AgentMessage request) { if (request instanceof AgentPing) { logger.debug("Got ping from server"); return new AgentPong((AgentPing) request); } else if (request instanceof AgentPong) { logger.debug("Got pong from server"); return null; } else if (request instanceof AgentError) { logger.warn("Got error from server: " + ((AgentError) request).getMessage()); return null; } else if (request != null) { AgentHandler handler = getHandler(request.getClass()); if (handler != null) { return handler.handle(request); } } return null; } }); } }); // connect the client logger.info("Connecting to: " + this.server); connectionAttempt.incrementAndGet(); b.connect(this.server.getHost(), (this.server.getPort() <= 0 ? 443 : this.server.getPort())).addListener(new GenericFutureListener<ChannelFuture>() { @Override public void operationComplete(ChannelFuture future) throws Exception { final Channel channel = future.channel(); if (future.isDone() && future.isSuccess()) { // stash the channel BergamotAgent.this.channel = channel; // reset fail counter connectionAttempt.set(0); // setup close listener channel.closeFuture().addListener(new GenericFutureListener<ChannelFuture>() { @Override public void operationComplete(ChannelFuture future) throws Exception { BergamotAgent.this.channel = null; logger.info("Connection closed."); BergamotAgent.this.scheduleReconnect(); } }); } else { logger.info("Failed to connect to: " + server + ""); // schedule reconnect BergamotAgent.this.scheduleReconnect(); } } }); } protected void scheduleReconnect() { long wait = Math.min(Math.max(connectionAttempt.get(), 1) * 1000L, 15000L); logger.info("Scheduling reconnection in " + wait + "ms"); this.timer.schedule(new TimerTask() { @Override public void run() { try { BergamotAgent.this.connect(); } catch (Exception e) { BergamotAgent.logger.error("Error connecting to server", e); BergamotAgent.this.scheduleReconnect(); } } }, wait); } public void shutdown() { try { this.eventLoop.shutdownGracefully().await(); } catch (InterruptedException e) { } } public void terminate() { try { this.eventLoop.shutdownGracefully(1, 2, TimeUnit.SECONDS).await(); } catch (InterruptedException e) { } } public void restart(final BergamotAgentCfg newConfig) { this.timer.schedule(new TimerTask() { @Override public void run() { try { BergamotAgent.this.terminate(); // reconfigure BergamotAgent.this.configure(newConfig); // start BergamotAgent.this.start(); } catch (Exception e) { BergamotAgent.logger.error("Failed to restart BergamotAgent", e); } } }, 500L); } private static void configureLogging() throws Exception { String logging = System.getProperty("bergamot.logging", "console"); if ("console".equals(logging)) { // configure logging to terminal BasicConfigurator.configure(); Logger.getRootLogger().setLevel(Level.toLevel(System.getProperty("bergamot.logging.level", "info").toUpperCase())); } else { // configure from file PropertyConfigurator.configure(new File(logging).getAbsolutePath()); } } public static BergamotAgentCfg readConfig() throws JAXBException, FileNotFoundException, IOException { // our possible configuration files for (File config : new File[] { new File(System.getProperty("bergamot.agent.config", "/etc/bergamot/agent.xml")), new File(System.getProperty("bergamot.agent.config.template", "/etc/bergamot/agent-template.xml"))}) { config = config.getAbsoluteFile(); logger.info("Trying configuration file: " + config.getAbsolutePath()); if (config.exists()) { FileInputStream input = new FileInputStream(config); try { return BergamotAgentCfg.read(BergamotAgentCfg.class, input); } finally { input.close(); } } } throw new FileNotFoundException("Failed to find bergamot agent configuration file"); } public static void saveConfig(BergamotAgentCfg newConfig) throws JAXBException, FileNotFoundException, IOException { File configFile = new File(System.getProperty("bergamot.agent.config", "/etc/bergamot/agent.xml")).getAbsoluteFile(); logger.info("Writing configuration to: " + configFile); // write the file FileOutputStream output = new FileOutputStream(configFile); try { BergamotAgentCfg.write(BergamotAgentCfg.class, newConfig, output); } finally { output.close(); } } public static void main(String[] args) throws Exception { // setup loggiing configureLogging(); // load our config BergamotAgentCfg config = readConfig(); // start the agent BergamotAgent agent = new BergamotAgent(); agent.configure(config); agent.start(); } }