blob: bcceef599e81b397954be76965ce82421c3eec2b [file] [log] [blame]
/*=========================================================================
* Copyright (c) 2010-2014 Pivotal Software, Inc. All Rights Reserved.
* This product is protected by U.S. and international copyright
* and intellectual property laws. Pivotal products are covered by
* one or more patents listed at http://www.pivotal.io/patents.
*=========================================================================
*/
package com.gemstone.gemfire.management.internal;
import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.lang.management.ManagementFactory;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;
import java.rmi.AlreadyBoundException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.RMIClientSocketFactory;
import java.rmi.server.RMIServerSocketFactory;
import java.rmi.server.UnicastRemoteObject;
import java.util.HashMap;
import javax.management.MBeanServer;
import javax.management.remote.JMXConnectorServer;
import javax.management.remote.JMXServiceURL;
import javax.management.remote.rmi.RMIConnectorServer;
import javax.management.remote.rmi.RMIJRMPServerImpl;
import javax.management.remote.rmi.RMIServerImpl;
import javax.rmi.ssl.SslRMIClientSocketFactory;
import org.apache.logging.log4j.Logger;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import com.gemstone.gemfire.cache.CacheFactory;
import com.gemstone.gemfire.distributed.internal.DistributionConfig;
import com.gemstone.gemfire.distributed.internal.DistributionManager;
import com.gemstone.gemfire.internal.GemFireVersion;
import com.gemstone.gemfire.internal.SocketCreator;
import com.gemstone.gemfire.internal.cache.GemFireCacheImpl;
import com.gemstone.gemfire.internal.lang.StringUtils;
import com.gemstone.gemfire.internal.logging.LogService;
import com.gemstone.gemfire.internal.tcp.TCPConduit;
import com.gemstone.gemfire.management.ManagementException;
import com.gemstone.gemfire.management.ManagementService;
import com.gemstone.gemfire.management.ManagerMXBean;
import com.gemstone.gemfire.management.internal.security.ManagementInterceptor;
import com.gemstone.gemfire.management.internal.unsafe.ReadOpFileAccessController;
/**
* Agent implementation that controls the JMX server end points for JMX
* clients to connect, such as an RMI server.
*
* The ManagementAgent could be used in a loner or GemFire client to define and
* control JMX server end points for the Platform MBeanServer and the GemFire
* MBeans hosted within it.
*
* @author VMware, Inc.
* @since 7.0
*/
public class ManagementAgent {
private static final Logger logger = LogService.getLogger();
/**
* True if running. Protected by synchronizing on this Manager instance. I
* used synchronization because I think we'll want to hold the same
* synchronize while configuring, starting, and eventually stopping the
* RMI server, the hidden management regions (in FederatingManager), etc
*/
private boolean running = false;
private Registry registry;
private JMXConnectorServer cs;
private final DistributionConfig config;
private boolean isHttpServiceRunning = false;
private ManagementInterceptor securityInterceptor;
/**
* This system property is set to true when the embedded HTTP server is started so that the embedded pulse webapp
* can use a local MBeanServer instead of a remote JMX connection.
*/
private static final String PULSE_EMBEDDED_PROP = "pulse.embedded";
public ManagementAgent(DistributionConfig config) {
this.config = config;
}
public synchronized boolean isRunning() {
return this.running;
}
public synchronized boolean isHttpServiceRunning() {
return isHttpServiceRunning;
}
public synchronized void setHttpServiceRunning(boolean isHttpServiceRunning) {
this.isHttpServiceRunning = isHttpServiceRunning;
}
private boolean isAPIRestServiceRunning(GemFireCacheImpl cache){
return (cache != null && cache.getRestAgent() != null && cache.getRestAgent().isRunning());
}
private boolean isServerNode(GemFireCacheImpl cache){
return (cache.getDistributedSystem().getDistributedMember().getVmKind() != DistributionManager.LOCATOR_DM_TYPE
&& cache.getDistributedSystem().getDistributedMember().getVmKind() != DistributionManager.ADMIN_ONLY_DM_TYPE
&& !cache.isClient());
}
public synchronized void startAgent(GemFireCacheImpl cache){
//Do not start Management REST service if developer REST service is already started.
if(!isAPIRestServiceRunning(cache)) {
startHttpService(isServerNode(cache));
}else {
if (logger.isDebugEnabled()) {
logger.debug("Developer REST APIs webapp is already running, Not Starting M&M REST and pulse!");
}
}
if (!this.running && this.config.getJmxManagerPort() != 0) {
try {
configureAndStart();
}
catch (IOException e) {
throw new ManagementException(e);
}
this.running = true;
}
}
public synchronized void stopAgent(){
stopHttpService();
if (!this.running) return;
if (logger.isDebugEnabled()) {
logger.debug("Stopping jmx manager agent");
}
try {
cs.stop();
UnicastRemoteObject.unexportObject(registry, true);
} catch (IOException e) {
throw new ManagementException(e);
}
this.running = false;
}
private Server httpServer;
private final String GEMFIRE_VERSION = GemFireVersion.getGemFireVersion();
private void startHttpService(boolean isServer) {
final SystemManagementService managementService = (SystemManagementService) ManagementService.getManagementService(
CacheFactory.getAnyInstance());
final ManagerMXBean managerBean = managementService.getManagerMXBean();
if (this.config.getHttpServicePort() != 0) {
if (logger.isDebugEnabled()) {
logger.debug("Attempting to start HTTP service on port ({}) at bind-address ({})...",
this.config.getHttpServicePort(), this.config.getHttpServiceBindAddress());
}
// GEMFIRE environment variable
final String gemfireHome = System.getenv("GEMFIRE");
// Check for empty variable. if empty, then log message and exit HTTP server startup
if (StringUtils.isBlank(gemfireHome)) {
final String message = "GEMFIRE environment variable not set; HTTP service will not start.";
setStatusMessage(managerBean, message);
if (logger.isDebugEnabled()) {
logger.debug(message);
}
return;
}
// Find the Management WAR file
final String gemfireWar = getGemFireWarLocation(gemfireHome);
if (gemfireWar == null) {
if (logger.isDebugEnabled()) {
logger.debug("Unable to find GemFire Management REST API WAR file; the Management REST Interface for GemFire will not be accessible.");
}
}
// Find the Pulse WAR file
final String pulseWar = getPulseWarLocation(gemfireHome);
if (pulseWar == null) {
final String message = "Unable to find Pulse web application WAR file; Pulse for GemFire will not be accessible";
setStatusMessage(managerBean, message);
if (logger.isDebugEnabled()) {
logger.debug(message);
}
}
//Find developer REST WAR file
final String gemfireAPIWar = getGemFireAPIWarLocation(gemfireHome);
if (gemfireAPIWar == null) {
final String message = "Unable to find GemFire Developer REST API WAR file; the Developer REST Interface for GemFire will not be accessible.";
setStatusMessage(managerBean, message);
if (logger.isDebugEnabled()) {
logger.debug(message);
}
}
try {
if (isWebApplicationAvailable(gemfireWar, pulseWar, gemfireAPIWar)) {
final String bindAddress = this.config.getHttpServiceBindAddress();
final int port = this.config.getHttpServicePort();
boolean isRestWebAppAdded = false;
this.httpServer = JettyHelper.initJetty(bindAddress, port,
this.config.getHttpServiceSSLEnabled(),
this.config.getHttpServiceSSLRequireAuthentication(),
this.config.getHttpServiceSSLProtocols(),
this.config.getHttpServiceSSLCiphers(),
this.config.getHttpServiceSSLProperties());
if (isWebApplicationAvailable(gemfireWar)) {
this.httpServer = JettyHelper.addWebApplication(this.httpServer, "/gemfire", gemfireWar);
}
if (isWebApplicationAvailable(pulseWar)) {
this.httpServer = JettyHelper.addWebApplication(this.httpServer, "/pulse", pulseWar);
}
if(isServer && this.config.getStartDevRestApi()) {
if (isWebApplicationAvailable(gemfireAPIWar) ) {
this.httpServer = JettyHelper.addWebApplication(this.httpServer, "/gemfire-api", gemfireAPIWar);
isRestWebAppAdded = true;
}
}else {
final String message = "Developer REST API web application will not start when start-dev-rest-api is not set and node is not server";
setStatusMessage(managerBean, message);
if (logger.isDebugEnabled()) {
logger.debug(message);
}
}
if (logger.isDebugEnabled()) {
logger.debug("Starting HTTP embedded server on port ({}) at bind-address ({})...",
((ServerConnector)this.httpServer.getConnectors()[0]).getPort(), bindAddress);
}
System.setProperty(PULSE_EMBEDDED_PROP, "true");
this.httpServer = JettyHelper.startJetty(this.httpServer);
// now, that Tomcat has been started, we can set the URL used by web clients to connect to Pulse
if (isWebApplicationAvailable(pulseWar)) {
managerBean.setPulseURL("http://".concat(getHost(bindAddress)).concat(":").concat(String.valueOf(port))
.concat("/pulse/"));
}
//set cache property for developer REST service running
if(isRestWebAppAdded) {
GemFireCacheImpl cache = (GemFireCacheImpl)CacheFactory.getAnyInstance();
cache.setRESTServiceRunning(true);
//create region to hold query information (queryId, queryString). Added for the developer REST APIs
RestAgent.createParameterizedQueryRegion();
}
//set true for HTTP service running
setHttpServiceRunning(true);
}
}
catch (Exception e) {
stopHttpService();//Jetty needs to be stopped even if it has failed to start. Some of the threads are left behind even if server.start() fails due to an exception
setStatusMessage(managerBean, "HTTP service failed to start with " + e.getClass().getSimpleName() + " '" + e.getMessage() + "'");
throw new ManagementException("HTTP service failed to start", e);
}
}
else {
setStatusMessage(managerBean, "Embedded HTTP server configured not to start (http-service-port=0) or (jmx-manager-http-port=0)");
}
}
private String getHost(final String bindAddress) throws UnknownHostException {
if (!StringUtils.isBlank(this.config.getJmxManagerHostnameForClients())) {
return this.config.getJmxManagerHostnameForClients();
}
else if (!StringUtils.isBlank(bindAddress)) {
return InetAddress.getByName(bindAddress).getHostAddress();
}
else {
return SocketCreator.getLocalHost().getHostAddress();
}
}
// Use the GEMFIRE environment variable to find the GemFire product tree.
// First, look in the $GEMFIRE/tools/Management directory
// Second, look in the $GEMFIRE/lib directory
// Finally, if we cannot find Management WAR file then return null...
private String getGemFireWarLocation(final String gemfireHome) {
assert !StringUtils.isBlank(gemfireHome) : "The GEMFIRE environment variable must be set!";
if (new File(gemfireHome + "/tools/Extensions/gemfire-web-" + GEMFIRE_VERSION + ".war").isFile()) {
return gemfireHome + "/tools/Extensions/gemfire-web-" + GEMFIRE_VERSION + ".war";
}
else if (new File(gemfireHome + "/lib/gemfire-web-" + GEMFIRE_VERSION + ".war").isFile()) {
return gemfireHome + "/lib/gemfire-web-" + GEMFIRE_VERSION + ".war";
}
else {
return null;
}
}
// Use the GEMFIRE environment variable to find the GemFire product tree.
// First, look in the $GEMFIRE/tools/Pulse directory
// Second, look in the $GEMFIRE/lib directory
// Finally, if we cannot find the Management WAR file then return null...
private String getPulseWarLocation(final String gemfireHome) {
assert !StringUtils.isBlank(gemfireHome) : "The GEMFIRE environment variable must be set!";
if (new File(gemfireHome + "/tools/Pulse/pulse.war").isFile()) {
return gemfireHome + "/tools/Pulse/pulse.war";
}
else if (new File(gemfireHome + "/lib/pulse.war").isFile()) {
return gemfireHome + "/lib/pulse.war";
}
else {
return null;
}
}
private String getGemFireAPIWarLocation(final String gemfireHome) {
assert !StringUtils.isBlank(gemfireHome) : "The GEMFIRE environment variable must be set!";
if (new File(gemfireHome + "/tools/Extensions/gemfire-web-api-" + GEMFIRE_VERSION + ".war").isFile()) {
return gemfireHome + "/tools/Extensions/gemfire-web-api-" + GEMFIRE_VERSION + ".war";
}
else if (new File(gemfireHome + "/lib/gemfire-web-api-" + GEMFIRE_VERSION + ".war").isFile()) {
return gemfireHome + "/lib/gemfire-web-api-" + GEMFIRE_VERSION + ".war";
}
else {
return null;
}
}
private boolean isRunningInTomcat() {
return (System.getProperty("catalina.base") != null || System.getProperty("catalina.home") != null);
}
private boolean isWebApplicationAvailable(final String warFileLocation) {
return !StringUtils.isBlank(warFileLocation);
}
private boolean isWebApplicationAvailable(final String... warFileLocations) {
for (String warFileLocation : warFileLocations) {
if (isWebApplicationAvailable(warFileLocation)) {
return true;
}
}
return false;
}
private void setStatusMessage(ManagerMXBean mBean, String message) {
mBean.setPulseURL("");
mBean.setStatusMessage(message);
}
private void stopHttpService() {
if (this.httpServer != null) {
if (logger.isDebugEnabled()) {
logger.debug("Stopping the HTTP service...");
}
try {
this.httpServer.stop();
} catch (Exception e) {
logger.warn("Failed to stop the HTTP service because: {}", e.getMessage(), e);
} finally {
try {
this.httpServer.destroy();
} catch (Exception ignore) {
logger.error("Failed to properly release resources held by the HTTP service: {}", ignore.getMessage(), ignore);
} finally {
this.httpServer = null;
System.clearProperty("catalina.base");
System.clearProperty("catalina.home");
}
}
}
}
/**
* http://docs.oracle.com/javase/6/docs/technotes/guides/management/agent.html#gdfvq
* https://blogs.oracle.com/jmxetc/entry/java_5_premain_rmi_connectors
* https://blogs.oracle.com/jmxetc/entry/building_a_remotely_stoppable_connector
* https://blogs.oracle.com/jmxetc/entry/jmx_connecting_through_firewalls_using
*/
private void configureAndStart() throws IOException {
// KIRK: I copied this from https://blogs.oracle.com/jmxetc/entry/java_5_premain_rmi_connectors
// we'll need to change this significantly but it's a starting point
// get the port for RMI Registry and RMI Connector Server
final int port = this.config.getJmxManagerPort();
final String hostname;
final InetAddress bindAddr;
if (this.config.getJmxManagerBindAddress().equals("")) {
hostname = SocketCreator.getLocalHost().getHostName();
bindAddr = null;
} else {
hostname = this.config.getJmxManagerBindAddress();
bindAddr = InetAddress.getByName(hostname);
}
final boolean ssl = this.config.getJmxManagerSSLEnabled();
if (logger.isDebugEnabled()) {
logger.debug("Starting jmx manager agent on port {}{}", port, (bindAddr != null ? (" bound to " + bindAddr) : "") + (ssl ? " using SSL" : ""));
}
final SocketCreator sc = SocketCreator.createNonDefaultInstance(ssl,
this.config.getJmxManagerSSLRequireAuthentication(),
this.config.getJmxManagerSSLProtocols(),
this.config.getJmxManagerSSLCiphers(),
this.config.getJmxSSLProperties());
RMIClientSocketFactory csf = ssl ? new SslRMIClientSocketFactory() : null;//RMISocketFactory.getDefaultSocketFactory();
//new GemFireRMIClientSocketFactory(sc, getLogger());
RMIServerSocketFactory ssf = new GemFireRMIServerSocketFactory(sc, bindAddr);
// Following is done to prevent rmi causing stop the world gcs
System.setProperty("sun.rmi.dgc.server.gcInterval", Long.toString(Long.MAX_VALUE-1));
// Create the RMI Registry using the SSL socket factories above.
// In order to use a single port, we must use these factories
// everywhere, or nowhere. Since we want to use them in the JMX
// RMI Connector server, we must also use them in the RMI Registry.
// Otherwise, we wouldn't be able to use a single port.
//
// Start an RMI registry on port <port>.
registry = LocateRegistry.createRegistry(port, csf, ssf);
// Retrieve the PlatformMBeanServer.
MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
// Environment map. KIRK: why is this declared as HashMap?
final HashMap<String,Object> env = new HashMap<String,Object>();
boolean integratedSecEnabled = System.getProperty("resource-authenticator") != null;
if (integratedSecEnabled) {
securityInterceptor = new ManagementInterceptor(logger);
env.put(JMXConnectorServer.AUTHENTICATOR, securityInterceptor);
} else {
/* Disable the old authenticator mechanism */
String pwFile = this.config.getJmxManagerPasswordFile();
if (pwFile != null && pwFile.length() > 0) {
env.put("jmx.remote.x.password.file", pwFile);
}
String accessFile = this.config.getJmxManagerAccessFile();
if (accessFile != null && accessFile.length() > 0) {
// Lets not use default connector based authorization
// env.put("jmx.remote.x.access.file", accessFile);
// Rewire the mbs hierarchy to set accessController
ReadOpFileAccessController controller = new ReadOpFileAccessController(accessFile);
controller.setMBeanServer(mbs);
mbs = controller;
}
}
// Manually creates and binds a JMX RMI Connector Server stub with the
// registry created above: the port we pass here is the port that can
// be specified in "service:jmx:rmi://"+hostname+":"+port - where the
// RMI server stub and connection objects will be exported.
// Here we choose to use the same port as was specified for the
// RMI Registry. We can do so because we're using \*the same\* client
// and server socket factories, for the registry itself \*and\* for this
// object.
final RMIServerImpl stub = new RMIJRMPServerImpl(port, csf, ssf, env);
// Create an RMI connector server.
//
// As specified in the JMXServiceURL the RMIServer stub will be
// registered in the RMI registry running in the local host on
// port <port> with the name "jmxrmi". This is the same name the
// out-of-the-box management agent uses to register the RMIServer
// stub too.
//
// The port specified in "service:jmx:rmi://"+hostname+":"+port
// is the second port, where RMI connection objects will be exported.
// Here we use the same port as that we choose for the RMI registry.
// The port for the RMI registry is specified in the second part
// of the URL, in "rmi://"+hostname+":"+port
//
// We construct a JMXServiceURL corresponding to what we have done
// for our stub...
final JMXServiceURL url = new JMXServiceURL(
"service:jmx:rmi://"+hostname+":"+port+"/jndi/rmi://"+hostname+":"+port+"/jmxrmi");
// Create an RMI connector server with the JMXServiceURL
//
// KIRK: JDK 1.5 cannot use JMXConnectorServerFactory because of
// http://bugs.sun.com/view_bug.do?bug_id=5107423
// but we're using JDK 1.6
cs = new RMIConnectorServer(new JMXServiceURL("rmi",hostname,port),
env,stub,mbs) {
@Override
public JMXServiceURL getAddress() { return url;}
@Override
public synchronized void start() throws IOException {
try {
registry.bind("jmxrmi", stub);
} catch (AlreadyBoundException x) {
final IOException io = new IOException(x.getMessage());
io.initCause(x);
throw io;
}
super.start();
}
};
// This may be the 1.6 way of doing it but the problem is it does not use our "stub".
//cs = JMXConnectorServerFactory.newJMXConnectorServer(url, env, mbs);
if (integratedSecEnabled) {
cs.setMBeanServerForwarder(securityInterceptor.getMBeanServerForwarder());
logger.info("Starting RMI Connector with Security Interceptor");
}
cs.start();
if (logger.isDebugEnabled()) {
logger.debug("Finished starting jmx manager agent.");
}
//System.out.println("Server started at: "+cs.getAddress());
// Start the CleanThread daemon... KIRK: not sure what CleanThread is...
//
//final Thread clean = new CleanThread(cs);
//clean.start();
}
private static class GemFireRMIClientSocketFactory implements RMIClientSocketFactory, Serializable {
private static final long serialVersionUID = -7604285019188827617L;
private /*final hack to prevent serialization*/ transient SocketCreator sc;
public GemFireRMIClientSocketFactory(SocketCreator sc) {
this.sc = sc;
}
@Override
public Socket createSocket(String host, int port) throws IOException {
return this.sc.connectForClient(host, port, 0/*no timeout*/);
}
};
private static class GemFireRMIServerSocketFactory implements RMIServerSocketFactory, Serializable {
private static final long serialVersionUID = -811909050641332716L;
private /*final hack to prevent serialization*/ transient SocketCreator sc;
private final InetAddress bindAddr;
public GemFireRMIServerSocketFactory(SocketCreator sc, InetAddress bindAddr) {
this.sc = sc;
this.bindAddr = bindAddr;
}
@Override
public ServerSocket createServerSocket(int port) throws IOException {
return this.sc.createServerSocket(port, TCPConduit.getBackLog(), this.bindAddr);
}
};
}