/* * Licensed to the Apache Software Foundation (ASF) under one or more contributor license * agreements. See the NOTICE file distributed with this work for additional information regarding * copyright ownership. The ASF licenses this file to You 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. */ package org.apache.geode.management.internal.cli.shell; import static org.apache.geode.distributed.ConfigurationProperties.CLUSTER_SSL_PREFIX; import static org.apache.geode.distributed.ConfigurationProperties.JMX_MANAGER_SSL_PREFIX; import org.apache.geode.distributed.internal.DistributionConfig; import org.apache.geode.internal.lang.StringUtils; import org.apache.geode.internal.util.ArrayUtils; import org.apache.geode.internal.util.IOUtils; import org.apache.geode.management.DistributedSystemMXBean; import org.apache.geode.management.MemberMXBean; import org.apache.geode.management.internal.MBeanJMXAdapter; import org.apache.geode.management.internal.ManagementConstants; import org.apache.geode.management.internal.cli.CliUtil; import org.apache.geode.management.internal.cli.CommandRequest; import org.apache.geode.management.internal.cli.LogWrapper; import org.apache.geode.management.internal.cli.commands.ShellCommands; import org.apache.geode.management.internal.cli.i18n.CliStrings; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; import java.net.URL; import java.text.MessageFormat; import java.util.Arrays; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Map.Entry; import java.util.Properties; import java.util.Set; import java.util.TreeSet; import java.util.concurrent.atomic.AtomicBoolean; import javax.management.AttributeNotFoundException; import javax.management.InstanceNotFoundException; import javax.management.JMX; import javax.management.MBeanException; import javax.management.MBeanServerConnection; import javax.management.MalformedObjectNameException; import javax.management.Notification; import javax.management.NotificationListener; import javax.management.ObjectName; import javax.management.QueryExp; import javax.management.ReflectionException; import javax.management.remote.JMXConnectionNotification; import javax.management.remote.JMXConnector; import javax.management.remote.JMXConnectorFactory; import javax.management.remote.JMXServiceURL; import javax.rmi.ssl.SslRMIClientSocketFactory; /** * OperationInvoker JMX Implementation * * @since GemFire 7.0 */ public class JmxOperationInvoker implements OperationInvoker { public static final String JMX_URL_FORMAT = "service:jmx:rmi://{0}/jndi/rmi://{0}:{1}/jmxrmi"; // an JMX object describing the client-end of a JMX connection private JMXConnector connector; // address of the JMX Connector Server private JMXServiceURL url; // an instance of an MBeanServer connection (in a connected state) private MBeanServerConnection mbsc; // String representation of the GemFire JMX Manager endpoint, including host and port private String endpoints; // the host and port of the GemFire Manager private String managerHost; private int managerPort; // MBean Proxies private DistributedSystemMXBean distributedSystemMXBeanProxy; private MemberMXBean memberMXBeanProxy; private ObjectName managerMemberObjectName; /* package */ final AtomicBoolean isConnected = new AtomicBoolean(false); /* package */ final AtomicBoolean isSelfDisconnect = new AtomicBoolean(false); private int clusterId = CLUSTER_ID_WHEN_NOT_CONNECTED; public JmxOperationInvoker(final String host, final int port, final String userName, final String password, final Map<String, String> sslConfigProps, String gfSecurityPropertiesPath) throws Exception { final Set<String> propsToClear = new TreeSet<String>(); try { this.managerHost = host; this.managerPort = port; this.endpoints = host + "[" + port + "]"; // Use the same syntax as the "connect" command. // Modify check period from default (60 sec) to 1 sec final Map<String, Object> env = new HashMap<String, Object>(); env.put(JMXConnectionListener.CHECK_PERIOD_PROP, JMXConnectionListener.CHECK_PERIOD); if (userName != null && userName.length() > 0) { env.put(JMXConnector.CREDENTIALS, new String[] {userName, password}); } Set<Entry<String, String>> entrySet = sslConfigProps.entrySet(); for (Iterator<Entry<String, String>> it = entrySet.iterator(); it.hasNext();) { Entry<String, String> entry = it.next(); String key = entry.getKey(); String value = entry.getValue(); key = checkforSystemPropertyPrefix(key); if ((key.equals(Gfsh.SSL_ENABLED_CIPHERS) || key.equals(Gfsh.SSL_ENABLED_PROTOCOLS)) && "any".equals(value)) { continue; } System.setProperty(key, value); propsToClear.add(key); } if (!sslConfigProps.isEmpty()) { if (System.getProperty(Gfsh.SSL_KEYSTORE) != null || System.getProperty(Gfsh.SSL_TRUSTSTORE) != null) { // use ssl to connect env.put("com.sun.jndi.rmi.factory.socket", new SslRMIClientSocketFactory()); } } // Check for JMX Credentials if empty put properties instance directly so that // jmx management interceptor can read it for custom security properties if (!env.containsKey(JMXConnector.CREDENTIALS)) { env.put(JMXConnector.CREDENTIALS, readProperties(gfSecurityPropertiesPath)); } this.url = new JMXServiceURL(MessageFormat.format(JMX_URL_FORMAT, checkAndConvertToCompatibleIPv6Syntax(host), String.valueOf(port))); this.connector = JMXConnectorFactory.connect(url, env); this.mbsc = connector.getMBeanServerConnection(); this.connector.addConnectionNotificationListener(new JMXConnectionListener(this), null, null); this.distributedSystemMXBeanProxy = JMX.newMXBeanProxy(mbsc, MBeanJMXAdapter.getDistributedSystemName(), DistributedSystemMXBean.class); if (this.distributedSystemMXBeanProxy == null) { LogWrapper.getInstance().info( "DistributedSystemMXBean is not present on member with endpoints : " + this.endpoints); throw new JMXConnectionException(JMXConnectionException.MANAGER_NOT_FOUND_EXCEPTION); } else { this.managerMemberObjectName = this.distributedSystemMXBeanProxy.getMemberObjectName(); if (this.managerMemberObjectName == null || !JMX.isMXBeanInterface(MemberMXBean.class)) { LogWrapper.getInstance() .info("MemberMXBean with ObjectName " + this.managerMemberObjectName + " is not present on member with endpoints : " + endpoints); throw new JMXConnectionException(JMXConnectionException.MANAGER_NOT_FOUND_EXCEPTION); } else { this.memberMXBeanProxy = JMX.newMXBeanProxy(mbsc, managerMemberObjectName, MemberMXBean.class); } } this.isConnected.set(true); this.clusterId = distributedSystemMXBeanProxy.getDistributedSystemId(); } catch (NullPointerException e) { throw e; } catch (MalformedURLException e) { throw e; } catch (IOException e) { throw e; } finally { for (String propToClear : propsToClear) { System.clearProperty(propToClear); } } } // Copied from ShellCommands.java private Properties readProperties(String gfSecurityPropertiesPath) throws MalformedURLException { Gfsh gfshInstance = Gfsh.getCurrentInstance(); // reference to hold resolved gfSecurityPropertiesPath String gfSecurityPropertiesPathToUse = CliUtil.resolvePathname(gfSecurityPropertiesPath); URL gfSecurityPropertiesUrl = null; // Case 1: User has specified gfSecurity properties file if (!StringUtils.isBlank(gfSecurityPropertiesPathToUse)) { // User specified gfSecurity properties doesn't exist if (!IOUtils.isExistingPathname(gfSecurityPropertiesPathToUse)) { gfshInstance .printAsSevere(CliStrings.format(CliStrings.GEODE_0_PROPERTIES_1_NOT_FOUND_MESSAGE, "Security ", gfSecurityPropertiesPathToUse)); } else { gfSecurityPropertiesUrl = new File(gfSecurityPropertiesPathToUse).toURI().toURL(); } } else if (gfSecurityPropertiesPath == null) { // Use default "gfsecurity.properties" // in current dir, user's home or classpath gfSecurityPropertiesUrl = ShellCommands.getFileUrl("gfsecurity.properties"); } // if 'gfSecurityPropertiesPath' OR gfsecurity.properties has resolvable path if (gfSecurityPropertiesUrl != null) { gfshInstance.logToFile("Using security properties file : " + CliUtil.decodeWithDefaultCharSet(gfSecurityPropertiesUrl.getPath()), null); return loadPropertiesFromURL(gfSecurityPropertiesUrl); } return null; } static Properties loadPropertiesFromURL(URL gfSecurityPropertiesUrl) { Properties props = new Properties(); if (gfSecurityPropertiesUrl != null) { InputStream inputStream = null; try { inputStream = gfSecurityPropertiesUrl.openStream(); props.load(inputStream); } catch (IOException io) { throw new RuntimeException( CliStrings.format(CliStrings.CONNECT__MSG__COULD_NOT_READ_CONFIG_FROM_0, CliUtil.decodeWithDefaultCharSet(gfSecurityPropertiesUrl.getPath())), io); } finally { IOUtils.close(inputStream); } } return props; } private String checkforSystemPropertyPrefix(String key) { String returnKey = key; if (key.startsWith("javax.")) { returnKey = key; } if (key.startsWith(CLUSTER_SSL_PREFIX) || key.startsWith(JMX_MANAGER_SSL_PREFIX) || key.startsWith(DistributionConfig.SSL_PREFIX)) { if (key.endsWith("keystore")) { returnKey = Gfsh.SSL_KEYSTORE; } else if (key.endsWith("keystore-password")) { returnKey = Gfsh.SSL_KEYSTORE_PASSWORD; } else if (key.endsWith("ciphers")) { returnKey = Gfsh.SSL_ENABLED_CIPHERS; } else if (key.endsWith("truststore-password")) { returnKey = Gfsh.SSL_TRUSTSTORE_PASSWORD; } else if (key.endsWith("truststore")) { returnKey = Gfsh.SSL_TRUSTSTORE; } else if (key.endsWith("protocols")) { returnKey = Gfsh.SSL_ENABLED_PROTOCOLS; } } return returnKey; } @Override public Object getAttribute(String resourceName, String attributeName) throws JMXInvocationException { try { return mbsc.getAttribute(ObjectName.getInstance(resourceName), attributeName); } catch (AttributeNotFoundException e) { throw new JMXInvocationException(attributeName + " not found for " + resourceName, e); } catch (InstanceNotFoundException e) { throw new JMXInvocationException(resourceName + " is not registered in the MBean server.", e); } catch (MalformedObjectNameException e) { throw new JMXInvocationException(resourceName + " is not a valid resource name.", e); } catch (MBeanException e) { throw new JMXInvocationException( "Exception while fetching " + attributeName + " for " + resourceName, e); } catch (ReflectionException e) { throw new JMXInvocationException("Couldn't find " + attributeName + " for " + resourceName, e); } catch (NullPointerException e) { throw new JMXInvocationException("Given resourceName is null.", e); } catch (IOException e) { throw new JMXInvocationException(resourceName + " is not a valid resource name.", e); } } @Override public Object invoke(String resourceName, String operationName, Object[] params, String[] signature) throws JMXInvocationException { try { return invoke(ObjectName.getInstance(resourceName), operationName, params, signature); } catch (MalformedObjectNameException e) { throw new JMXInvocationException(resourceName + " is not a valid resource name.", e); } catch (NullPointerException e) { throw new JMXInvocationException("Given resourceName is null.", e); } } /** * JMX Specific operation invoke caller. * * @param resource * @param operationName * @param params * @param signature * * @return result of JMX Operation invocation * * @throws JMXInvocationException */ protected Object invoke(ObjectName resource, String operationName, Object[] params, String[] signature) throws JMXInvocationException { try { return mbsc.invoke(resource, operationName, params, signature); } catch (InstanceNotFoundException e) { throw new JMXInvocationException(resource + " is not registered in the MBean server.", e); } catch (MBeanException e) { throw new JMXInvocationException( "Exception while invoking " + operationName + " on " + resource, e); } catch (ReflectionException e) { throw new JMXInvocationException("Couldn't find " + operationName + " on " + resource + " with arguments " + Arrays.toString(signature), e); } catch (IOException e) { throw new JMXInvocationException("Couldn't communicate with remote server at " + toString(), e); } } public Set<ObjectName> queryNames(final ObjectName objectName, final QueryExp queryExpression) { try { return getMBeanServerConnection().queryNames(objectName, queryExpression); } catch (IOException e) { throw new JMXInvocationException(String .format("Failed to communicate with the remote MBean server at (%1$s)!", toString()), e); } } @Override public Object processCommand(final CommandRequest commandRequest) throws JMXInvocationException { // Gfsh.getCurrentInstance().printAsSevere(String.format("Command (%1$s)%n", // commandRequest.getInput())); if (commandRequest.hasFileData()) { return memberMXBeanProxy.processCommand(commandRequest.getInput(), commandRequest.getEnvironment(), ArrayUtils.toByteArray(commandRequest.getFileData())); } else { return memberMXBeanProxy.processCommand(commandRequest.getInput(), commandRequest.getEnvironment()); } } @Override public void stop() { try { this.isSelfDisconnect.set(true); this.connector.close(); this.isConnected.set(false); } catch (IOException e) { // ignore exceptions occurring while closing the connector } } @Override public boolean isConnected() { return this.isConnected.get(); } public DistributedSystemMXBean getDistributedSystemMXBean() { if (distributedSystemMXBeanProxy == null) { throw new IllegalStateException( "The DistributedSystemMXBean proxy was not initialized properly!"); } return distributedSystemMXBeanProxy; } public JMXServiceURL getJmxServiceUrl() { return this.url; } public String getManagerHost() { return managerHost; } public int getManagerPort() { return managerPort; } public <T> T getMBeanProxy(final ObjectName objectName, final Class<T> mbeanInterface) { if (DistributedSystemMXBean.class.equals(mbeanInterface) && ManagementConstants.OBJECTNAME__DISTRIBUTEDSYSTEM_MXBEAN.equals(objectName.toString())) { return mbeanInterface.cast(getDistributedSystemMXBean()); } else if (JMX.isMXBeanInterface(mbeanInterface)) { return JMX.newMXBeanProxy(getMBeanServerConnection(), objectName, mbeanInterface); } else { return JMX.newMBeanProxy(getMBeanServerConnection(), objectName, mbeanInterface); } } public MBeanServerConnection getMBeanServerConnection() { if (this.mbsc == null) { throw new IllegalStateException("Gfsh is not connected to the GemFire Manager."); } return this.mbsc; } public boolean isReady() { try { return this.mbsc.isRegistered(managerMemberObjectName); } catch (IOException e) { return false; } } @Override public String toString() { return this.endpoints; } public int getClusterId() { return this.clusterId; } /* package */ void resetClusterId() { clusterId = CLUSTER_ID_WHEN_NOT_CONNECTED; } /** * If the given host address contains a ":", considers it as an IPv6 address & returns the host * based on RFC2732 requirements i.e. surrounds the given host address string with square * brackets. If ":" is not found in the given string, simply returns the same string. * * @param hostAddress host address to check if it's an IPv6 address * * @return for an IPv6 address returns compatible host address otherwise returns the same string */ // TODO - Abhishek: move to utility class // Taken from GFMon public static String checkAndConvertToCompatibleIPv6Syntax(String hostAddress) { // if host string contains ":", considering it as an IPv6 Address // Conforming to RFC2732 - http://www.ietf.org/rfc/rfc2732.txt if (hostAddress.indexOf(":") != -1) { LogWrapper logger = LogWrapper.getInstance(); if (logger.fineEnabled()) { logger.fine("IPv6 host address detected, using IPv6 syntax for host in JMX connection URL"); } hostAddress = "[" + hostAddress + "]"; if (logger.fineEnabled()) { logger.fine("Compatible host address is : " + hostAddress); } } return hostAddress; } } /** * A Connection Notification Listener. Notifies Gfsh when a connection gets terminated abruptly. * * @since GemFire 7.0 */ class JMXConnectionListener implements NotificationListener { public static final String CHECK_PERIOD_PROP = "jmx.remote.x.client.connection.check.period"; public static final long CHECK_PERIOD = 1000L; private JmxOperationInvoker invoker; JMXConnectionListener(JmxOperationInvoker invoker) { this.invoker = invoker; } @Override public void handleNotification(Notification notification, Object handback) { if (JMXConnectionNotification.class.isInstance(notification)) { JMXConnectionNotification connNotif = (JMXConnectionNotification) notification; if (JMXConnectionNotification.CLOSED.equals(connNotif.getType()) || JMXConnectionNotification.FAILED.equals(connNotif.getType())) { this.invoker.isConnected.set(false); this.invoker.resetClusterId(); if (!this.invoker.isSelfDisconnect.get()) { Gfsh.getCurrentInstance().notifyDisconnect(this.invoker.toString()); } } } } }