http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/main/java/org/apache/brooklyn/entity/java/JavaSoftwareProcessSshDriver.java ---------------------------------------------------------------------- diff --git a/software/base/src/main/java/org/apache/brooklyn/entity/java/JavaSoftwareProcessSshDriver.java b/software/base/src/main/java/org/apache/brooklyn/entity/java/JavaSoftwareProcessSshDriver.java new file mode 100644 index 0000000..f162ca4 --- /dev/null +++ b/software/base/src/main/java/org/apache/brooklyn/entity/java/JavaSoftwareProcessSshDriver.java @@ -0,0 +1,443 @@ +/* + * 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.brooklyn.entity.java; + +import static org.apache.brooklyn.util.JavaGroovyEquivalents.groovyTruth; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.brooklyn.api.internal.EntityLocal; +import org.apache.brooklyn.effector.core.EffectorTasks; +import org.apache.brooklyn.entity.core.Attributes; +import org.apache.brooklyn.entity.core.Entities; +import org.apache.brooklyn.entity.core.EntityInternal; +import org.apache.brooklyn.entity.software.base.AbstractSoftwareProcessSshDriver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.base.Function; +import com.google.common.base.Joiner; +import com.google.common.base.Optional; +import com.google.common.base.Splitter; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.gson.internal.Primitives; + +import org.apache.brooklyn.location.basic.SshMachineLocation; +import org.apache.brooklyn.sensor.ssh.SshEffectorTasks; +import org.apache.brooklyn.util.collections.MutableMap; +import org.apache.brooklyn.util.collections.MutableSet; +import org.apache.brooklyn.util.core.flags.TypeCoercions; +import org.apache.brooklyn.util.core.internal.ssh.ShellTool; +import org.apache.brooklyn.util.core.task.DynamicTasks; +import org.apache.brooklyn.util.core.task.Tasks; +import org.apache.brooklyn.util.core.task.ssh.SshTasks; +import org.apache.brooklyn.util.core.task.system.ProcessTaskFactory; +import org.apache.brooklyn.util.core.task.system.ProcessTaskWrapper; +import org.apache.brooklyn.util.exceptions.Exceptions; +import org.apache.brooklyn.util.ssh.BashCommands; +import org.apache.brooklyn.util.text.Strings; +import org.apache.brooklyn.util.text.StringEscapes.BashStringEscapes; + +/** + * The SSH implementation of the {@link org.apache.brooklyn.entity.java.JavaSoftwareProcessDriver}. + */ +public abstract class JavaSoftwareProcessSshDriver extends AbstractSoftwareProcessSshDriver implements JavaSoftwareProcessDriver { + + public static final Logger log = LoggerFactory.getLogger(JavaSoftwareProcessSshDriver.class); + + public static final List<List<String>> MUTUALLY_EXCLUSIVE_OPTS = ImmutableList.<List<String>> of(ImmutableList.of("-client", + "-server")); + + public static final List<String> KEY_VAL_OPT_PREFIXES = ImmutableList.of("-Xmx", "-Xms", "-Xss"); + + public JavaSoftwareProcessSshDriver(EntityLocal entity, SshMachineLocation machine) { + super(entity, machine); + + entity.setAttribute(Attributes.LOG_FILE_LOCATION, getLogFileLocation()); + } + + protected abstract String getLogFileLocation(); + + public boolean isJmxEnabled() { + return (entity instanceof UsesJmx) && (entity.getConfig(UsesJmx.USE_JMX)); + } + + public boolean isJmxSslEnabled() { + return isJmxEnabled() && groovyTruth(entity.getConfig(UsesJmx.JMX_SSL_ENABLED)); + } + + /** + * Sets all JVM options (-X.. -D..) in an environment var JAVA_OPTS. + * <p> + * That variable is constructed from {@link #getJavaOpts()}, then wrapped _unescaped_ in double quotes. An + * error is thrown if there is an unescaped double quote in the string. All other unescaped + * characters are permitted, but unless $var expansion or `command` execution is desired (although + * this is not confirmed as supported) the generally caller should escape any such characters, for + * example using {@link BashStringEscapes#escapeLiteralForDoubleQuotedBash(String)}. + */ + @Override + public Map<String, String> getShellEnvironment() { + List<String> javaOpts = getJavaOpts(); + + for (String it : javaOpts) { + BashStringEscapes.assertValidForDoubleQuotingInBash(it); + } + // do not double quote here; the env var is double quoted subsequently; + // spaces should be preceded by double-quote + // (if dbl quotes are needed we could pass on the command-line instead of in an env var) + String sJavaOpts = Joiner.on(' ').join(javaOpts); + return MutableMap.<String, String>builder().putAll(super.getShellEnvironment()).put("JAVA_OPTS", sJavaOpts).build(); + } + + /** + * arguments to pass to the JVM; this is the config options (e.g. -Xmx1024; only the contents of + * {@link #getCustomJavaConfigOptions()} by default) and java system properties (-Dk=v; add custom + * properties in {@link #getCustomJavaSystemProperties()}) + * <p> + * See {@link #getShellEnvironment()} for discussion of quoting/escaping strategy. + **/ + public List<String> getJavaOpts() { + Iterable<String> sysprops = Iterables.transform(getJavaSystemProperties().entrySet(), + new Function<Map.Entry<String, ?>, String>() { + public String apply(Map.Entry<String, ?> entry) { + String k = entry.getKey(); + Object v = entry.getValue(); + try { + if (v != null && Primitives.isWrapperType(v.getClass())) { + v = "" + v; + } else { + v = Tasks.resolveValue(v, Object.class, ((EntityInternal)entity).getExecutionContext()); + if (v == null) { + } else if (v instanceof CharSequence) { + } else if (TypeCoercions.isPrimitiveOrBoxer(v.getClass())) { + v = "" + v; + } else { + // could do toString, but that's likely not what is desired; + // probably a type mismatch, + // post-processing should be specified (common types are accepted + // above) + throw new IllegalArgumentException("cannot convert value " + v + " of type " + v.getClass() + + " to string to pass as JVM property; use a post-processor"); + } + } + return "-D" + k + (v != null ? "=" + v : ""); + } catch (Exception e) { + log.warn("Error resolving java option key {}, propagating: {}", k, e); + throw Throwables.propagate(e); + } + } + }); + + Set<String> result = MutableSet.<String> builder(). + addAll(getJmxJavaConfigOptions()). + addAll(getCustomJavaConfigOptions()). + addAll(sysprops). + build(); + + for (String customOpt : entity.getConfig(UsesJava.JAVA_OPTS)) { + for (List<String> mutuallyExclusiveOpt : MUTUALLY_EXCLUSIVE_OPTS) { + if (mutuallyExclusiveOpt.contains(customOpt)) { + result.removeAll(mutuallyExclusiveOpt); + } + } + for (String keyValOptPrefix : KEY_VAL_OPT_PREFIXES) { + if (customOpt.startsWith(keyValOptPrefix)) { + for (Iterator<String> iter = result.iterator(); iter.hasNext();) { + String existingOpt = iter.next(); + if (existingOpt.startsWith(keyValOptPrefix)) { + iter.remove(); + } + } + } + } + if (customOpt.contains("=")) { + String customOptPrefix = customOpt.substring(0, customOpt.indexOf("=")); + + for (Iterator<String> iter = result.iterator(); iter.hasNext();) { + String existingOpt = iter.next(); + if (existingOpt.startsWith(customOptPrefix)) { + iter.remove(); + } + } + } + result.add(customOpt); + } + + return ImmutableList.copyOf(result); + } + + /** + * Returns the complete set of Java system properties (-D defines) to set for the application. + * <p> + * This is exposed to the JVM as the contents of the {@code JAVA_OPTS} environment variable. Default + * set contains config key, custom system properties, and JMX defines. + * <p> + * Null value means to set -Dkey otherwise it is -Dkey=value. + * <p> + * See {@link #getShellEnvironment()} for discussion of quoting/escaping strategy. + */ + protected Map<String,?> getJavaSystemProperties() { + return MutableMap.<String,Object>builder() + .putAll(getCustomJavaSystemProperties()) + .putAll(isJmxEnabled() ? getJmxJavaSystemProperties() : Collections.<String,Object>emptyMap()) + .putAll(entity.getConfig(UsesJava.JAVA_SYSPROPS)) + .build(); + } + + /** + * Return extra Java system properties (-D defines) used by the application. + * + * Override as needed; default is an empty map. + */ + protected Map getCustomJavaSystemProperties() { + return Maps.newLinkedHashMap(); + } + + /** + * Return extra Java config options, ie arguments starting with - which are passed to the JVM prior + * to the class name. + * <p> + * Note defines are handled separately, in {@link #getCustomJavaSystemProperties()}. + * <p> + * Override as needed; default is an empty list. + */ + protected List<String> getCustomJavaConfigOptions() { + return Lists.newArrayList(); + } + + /** @deprecated since 0.6.0, the config key is always used instead of this */ @Deprecated + public Integer getJmxPort() { + return !isJmxEnabled() ? Integer.valueOf(-1) : entity.getAttribute(UsesJmx.JMX_PORT); + } + + /** @deprecated since 0.6.0, the config key is always used instead of this */ @Deprecated + public Integer getRmiRegistryPort() { + return !isJmxEnabled() ? -1 : entity.getAttribute(UsesJmx.RMI_REGISTRY_PORT); + } + + /** @deprecated since 0.6.0, the config key is always used instead of this */ @Deprecated + public String getJmxContext() { + return !isJmxEnabled() ? null : entity.getAttribute(UsesJmx.JMX_CONTEXT); + } + + /** + * Return the configuration properties required to enable JMX for a Java application. + * + * These should be set as properties in the {@code JAVA_OPTS} environment variable when calling the + * run script for the application. + */ + protected Map<String, ?> getJmxJavaSystemProperties() { + MutableMap.Builder<String, Object> result = MutableMap.<String, Object> builder(); + + if (isJmxEnabled()) { + new JmxSupport(getEntity(), getRunDir()).applyJmxJavaSystemProperties(result); + } + + return result.build(); + } + + /** + * Return any JVM arguments required, other than the -D defines returned by {@link #getJmxJavaSystemProperties()} + */ + protected List<String> getJmxJavaConfigOptions() { + List<String> result = new ArrayList<String>(); + if (isJmxEnabled()) { + result.addAll(new JmxSupport(getEntity(), getRunDir()).getJmxJavaConfigOptions()); + } + return result; + } + + /** + * Checks for the presence of Java on the entity's location, installing if necessary. + * @return true if the required version of Java was found on the machine or if it was installed correctly, + * otherwise false. + */ + protected boolean checkForAndInstallJava(String requiredVersion) { + int requiredJavaMinor; + if (requiredVersion.contains(".")) { + List<String> requiredVersionParts = Splitter.on(".").splitToList(requiredVersion); + requiredJavaMinor = Integer.valueOf(requiredVersionParts.get(1)); + } else if (requiredVersion.length() == 1) { + requiredJavaMinor = Integer.valueOf(requiredVersion); + } else { + log.error("java version required {} is not supported", requiredVersion); + throw new IllegalArgumentException("Required java version " + requiredVersion + " not supported"); + } + Optional<String> installedJavaVersion = getInstalledJavaVersion(); + if (installedJavaVersion.isPresent()) { + List<String> installedVersionParts = Splitter.on(".").splitToList(installedJavaVersion.get()); + int javaMajor = Integer.valueOf(installedVersionParts.get(0)); + int javaMinor = Integer.valueOf(installedVersionParts.get(1)); + if (javaMajor == 1 && javaMinor >= requiredJavaMinor) { + log.debug("Java {} already installed at {}@{}", new Object[]{installedJavaVersion.get(), getEntity(), getLocation()}); + return true; + } + } + return tryJavaInstall(requiredVersion, BashCommands.installJava(requiredJavaMinor)) == 0; + } + + protected int tryJavaInstall(String version, String command) { + getLocation().acquireMutex("installing", "installing Java at " + getLocation()); + try { + log.debug("Installing Java {} at {}@{}", new Object[]{version, getEntity(), getLocation()}); + ProcessTaskFactory<Integer> taskFactory = SshTasks.newSshExecTaskFactory(getLocation(), command) + .summary("install java ("+version+")") + .configure(ShellTool.PROP_EXEC_ASYNC, true); + ProcessTaskWrapper<Integer> installCommand = Entities.submit(getEntity(), taskFactory); + int result = installCommand.get(); + if (result != 0) { + log.warn("Installation of Java {} failed at {}@{}: {}", + new Object[]{version, getEntity(), getLocation(), installCommand.getStderr()}); + } + return result; + } finally { + getLocation().releaseMutex("installing"); + } + } + + /** + * @deprecated since 0.7.0; instead use {@link #getInstalledJavaVersion()} + */ + @Deprecated + protected Optional<String> getCurrentJavaVersion() { + return getInstalledJavaVersion(); + } + + /** + * Checks for the version of Java installed on the entity's location over SSH. + * @return An Optional containing the version portion of `java -version`, or absent if no Java found. + */ + protected Optional<String> getInstalledJavaVersion() { + log.debug("Checking Java version at {}@{}", getEntity(), getLocation()); + // sed gets stdin like 'java version "1.7.0_45"' + ProcessTaskWrapper<Integer> versionCommand = Entities.submit(getEntity(), SshTasks.newSshExecTaskFactory( + getLocation(), "java -version 2>&1 | grep \" version\" | sed 's/.*\"\\(.*\\).*\"/\\1/'")); + versionCommand.get(); + String stdOut = versionCommand.getStdout().trim(); + if (!Strings.isBlank(stdOut)) { + log.debug("Found Java version at {}@{}: {}", new Object[] {getEntity(), getLocation(), stdOut}); + return Optional.of(stdOut); + } else { + log.debug("Found no Java installed at {}@{}", getEntity(), getLocation()); + return Optional.absent(); + } + } + + /** + * Answers one of "OpenJDK", "Oracle", or other vendor info. + */ + protected Optional<String> getCurrentJavaVendor() { + // TODO Also handle IBM jvm + log.debug("Checking Java vendor at {}@{}", getEntity(), getLocation()); + ProcessTaskWrapper<Integer> versionCommand = Entities.submit(getEntity(), SshTasks.newSshExecTaskFactory( + getLocation(), "java -version 2>&1 | awk 'NR==2 {print $1}'")); + versionCommand.get(); + String stdOut = versionCommand.getStdout().trim(); + if (Strings.isBlank(stdOut)) { + log.debug("Found no Java installed at {}@{}", getEntity(), getLocation()); + return Optional.absent(); + } else if ("Java(TM)".equals(stdOut)) { + log.debug("Found Java version at {}@{}: {}", new Object[] {getEntity(), getLocation(), stdOut}); + return Optional.of("Oracle"); + } else { + return Optional.of(stdOut); + } + } + + /** + * Checks for Java 6 or 7, installing Java 7 if neither are found. Override this method to + * check for and install specific versions of Java. + * + * @see #checkForAndInstallJava(String) + */ + public boolean installJava() { + if (entity instanceof UsesJava) { + String version = entity.getConfig(UsesJava.JAVA_VERSION_REQUIRED); + return checkForAndInstallJava(version); + } + // by default it installs jdk7 + return checkForAndInstallJava("1.7"); + } + + public void installJmxSupport() { + if (isJmxEnabled()) { + newScript("JMX_SETUP_PREINSTALL").body.append("mkdir -p "+getRunDir()).execute(); + new JmxSupport(getEntity(), getRunDir()).install(); + } + } + + public void checkJavaHostnameBug() { + checkNoHostnameBug(); + + try { + ProcessTaskWrapper<Integer> hostnameTask = DynamicTasks.queue(SshEffectorTasks.ssh("echo FOREMARKER; hostname -f; echo AFTMARKER")).block(); + String stdout = Strings.getFragmentBetween(hostnameTask.getStdout(), "FOREMARKER", "AFTMARKER"); + if (hostnameTask.getExitCode() == 0 && Strings.isNonBlank(stdout)) { + String hostname = stdout.trim(); + Integer len = hostname.length(); + if (len > 63) { + // likely to cause a java crash due to java bug 7089443 -- set a new short hostname + // http://mail.openjdk.java.net/pipermail/net-dev/2012-July/004603.html + String newHostname = "br-"+getEntity().getId().toLowerCase(); + log.info("Detected likelihood of Java hostname bug with hostname length "+len+" for "+getEntity()+"; renaming "+getMachine()+" to hostname "+newHostname); + DynamicTasks.queue(SshEffectorTasks.ssh(BashCommands.setHostname(newHostname, null))).block(); + } + } else { + log.debug("Hostname length could not be determined for location "+EffectorTasks.findSshMachine()+"; not doing Java hostname bug check"); + } + } catch (Exception e) { + Exceptions.propagateIfFatal(e); + log.warn("Error checking/fixing Java hostname bug (continuing): "+e, e); + } + } + + @Override + public void setup() { + DynamicTasks.queue("install java", new Runnable() { public void run() { + installJava(); + }}); + + // TODO check java version + + if (getEntity().getConfig(UsesJava.CHECK_JAVA_HOSTNAME_BUG)) { + DynamicTasks.queue("check java hostname bug", new Runnable() { public void run() { + checkJavaHostnameBug(); }}); + } + } + + @Override + public void copyRuntimeResources() { + super.copyRuntimeResources(); + + if (isJmxEnabled()) { + DynamicTasks.queue("install jmx", new Runnable() { public void run() { + installJmxSupport(); }}); + } + } + +}
http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/main/java/org/apache/brooklyn/entity/java/JmxAttributeSensor.java ---------------------------------------------------------------------- diff --git a/software/base/src/main/java/org/apache/brooklyn/entity/java/JmxAttributeSensor.java b/software/base/src/main/java/org/apache/brooklyn/entity/java/JmxAttributeSensor.java new file mode 100644 index 0000000..bee8126 --- /dev/null +++ b/software/base/src/main/java/org/apache/brooklyn/entity/java/JmxAttributeSensor.java @@ -0,0 +1,121 @@ +/* + * 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.brooklyn.entity.java; + +import java.util.concurrent.Callable; + +import javax.management.MalformedObjectNameException; +import javax.management.ObjectName; + +import org.apache.brooklyn.api.internal.EntityLocal; +import org.apache.brooklyn.api.mgmt.Task; +import org.apache.brooklyn.config.ConfigKey; +import org.apache.brooklyn.core.config.ConfigKeys; +import org.apache.brooklyn.effector.core.AddSensor; +import org.apache.brooklyn.sensor.core.DependentConfiguration; +import org.apache.brooklyn.sensor.core.HttpRequestSensor; +import org.apache.brooklyn.sensor.feed.jmx.JmxAttributePollConfig; +import org.apache.brooklyn.sensor.feed.jmx.JmxFeed; +import org.apache.brooklyn.sensor.feed.jmx.JmxHelper; +import org.apache.brooklyn.sensor.ssh.SshCommandSensor; +import org.apache.brooklyn.util.core.config.ConfigBag; +import org.apache.brooklyn.util.core.task.DynamicTasks; +import org.apache.brooklyn.util.core.task.Tasks; +import org.apache.brooklyn.util.time.Duration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.annotations.Beta; +import com.google.common.base.Functions; +import com.google.common.base.Preconditions; + +/** + * Configurable {@link org.apache.brooklyn.api.entity.EntityInitializer} which adds a JMX sensor feed to retrieve an + * <code>attribute</code> from a JMX <code>objectName</code>. + * + * @see SshCommandSensor + * @see HttpRequestSensor + */ +@Beta +public final class JmxAttributeSensor<T> extends AddSensor<T> { + + private static final Logger LOG = LoggerFactory.getLogger(JmxAttributeSensor.class); + + public static final ConfigKey<String> OBJECT_NAME = ConfigKeys.newStringConfigKey("objectName", "JMX object name for sensor lookup"); + public static final ConfigKey<String> ATTRIBUTE = ConfigKeys.newStringConfigKey("attribute", "JMX attribute to poll in object"); + public static final ConfigKey<Object> DEFAULT_VALUE = ConfigKeys.newConfigKey(Object.class, "defaultValue", "Default value for sensor; normally null"); + + protected final String objectName; + protected final String attribute; + protected final Object defaultValue; + + public JmxAttributeSensor(final ConfigBag params) { + super(params); + + objectName = Preconditions.checkNotNull(params.get(OBJECT_NAME), "objectName"); + attribute = Preconditions.checkNotNull(params.get(ATTRIBUTE), "attribute"); + defaultValue = params.get(DEFAULT_VALUE); + + try { + ObjectName.getInstance(objectName); + } catch (MalformedObjectNameException mone) { + throw new IllegalArgumentException("Malformed JMX object name: " + objectName, mone); + } + } + + @Override + public void apply(final EntityLocal entity) { + super.apply(entity); + + if (entity instanceof UsesJmx) { + if (LOG.isDebugEnabled()) { + LOG.debug("Submitting task to add JMX sensor {} to {}", name, entity); + } + + Task<Integer> jmxPortTask = DependentConfiguration.attributeWhenReady(entity, UsesJmx.JMX_PORT); + Task<JmxFeed> jmxFeedTask = Tasks.<JmxFeed>builder() + .description("Add JMX feed") + .body(new Callable<JmxFeed>() { + @Override + public JmxFeed call() throws Exception { + JmxHelper helper = new JmxHelper(entity); + Duration period = entity.getConfig(SENSOR_PERIOD); + + JmxFeed feed = JmxFeed.builder() + .entity(entity) + .period(period) + .helper(helper) + .pollAttribute(new JmxAttributePollConfig<T>(sensor) + .objectName(objectName) + .attributeName(attribute) + .onFailureOrException(Functions.<T>constant((T) defaultValue))) + .build(); + return feed; + } + }) + .build(); + DynamicTasks.submit(Tasks.sequential("Add JMX Sensor " + sensor.getName(), jmxPortTask, jmxFeedTask), entity); + } else { + throw new IllegalStateException(String.format("Entity %s does not support JMX", entity)); + } + + // TODO add entity shutdown hook to stop JmxFeed + } + +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/main/java/org/apache/brooklyn/entity/java/JmxSupport.java ---------------------------------------------------------------------- diff --git a/software/base/src/main/java/org/apache/brooklyn/entity/java/JmxSupport.java b/software/base/src/main/java/org/apache/brooklyn/entity/java/JmxSupport.java new file mode 100644 index 0000000..5e67d30 --- /dev/null +++ b/software/base/src/main/java/org/apache/brooklyn/entity/java/JmxSupport.java @@ -0,0 +1,357 @@ +/* + * 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.brooklyn.entity.java; + +import java.util.EnumSet; +import java.util.List; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import org.apache.brooklyn.api.entity.Entity; +import org.apache.brooklyn.api.internal.EntityLocal; +import org.apache.brooklyn.config.ConfigKey; +import org.apache.brooklyn.config.ConfigKey.HasConfigKey; +import org.apache.brooklyn.entity.core.EntityInternal; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.apache.brooklyn.location.access.BrooklynAccessUtils; +import org.apache.brooklyn.location.basic.Locations; +import org.apache.brooklyn.location.basic.SshMachineLocation; +import org.apache.brooklyn.sensor.feed.jmx.JmxHelper; +import org.apache.brooklyn.util.collections.MutableList; +import org.apache.brooklyn.util.collections.MutableMap; +import org.apache.brooklyn.util.core.BrooklynMavenArtifacts; +import org.apache.brooklyn.util.core.ResourceUtils; +import org.apache.brooklyn.util.core.task.Tasks; +import org.apache.brooklyn.util.guava.Maybe; +import org.apache.brooklyn.util.jmx.jmxmp.JmxmpAgent; +import org.apache.brooklyn.util.jmx.jmxrmi.JmxRmiAgent; +import org.apache.brooklyn.util.maven.MavenArtifact; +import org.apache.brooklyn.util.maven.MavenRetriever; +import org.apache.brooklyn.util.net.Urls; +import org.apache.brooklyn.util.text.Strings; + +import com.google.common.base.Preconditions; +import com.google.common.net.HostAndPort; + +public class JmxSupport implements UsesJmx { + + private static final Logger log = LoggerFactory.getLogger(JmxSupport.class); + + private final Entity entity; + private final String runDir; + + private Boolean isJmx; + private Boolean isSecure; + private JmxAgentModes jmxAgentMode; + + private static boolean warnedAboutNotOnClasspath = false; + + /** run dir may be null if it is not accessed */ + public JmxSupport(Entity entity, @Nullable String runDir) { + this.entity = Preconditions.checkNotNull(entity, "entity must be supplied"); + this.runDir = runDir; + } + + @Nonnull + public String getRunDir() { + return Preconditions.checkNotNull(runDir, "runDir must have been supplied to perform this operation"); + } + + public Entity getEntity() { + return entity; + } + + <T> T getConfig(ConfigKey<T> key) { + return getEntity().getConfig(key); + } + + <T> T getConfig(HasConfigKey<T> key) { + return getEntity().getConfig(key); + } + + <T> void setConfig(ConfigKey<T> key, T value) { + ((EntityLocal)getEntity()).setConfig(key, value); + } + + public Maybe<SshMachineLocation> getMachine() { + return Locations.findUniqueSshMachineLocation(entity.getLocations()); + } + + public boolean isJmx() { + init(); + return isJmx; + } + + public JmxAgentModes getJmxAgentMode() { + init(); + if (jmxAgentMode==null) return JmxAgentModes.NONE; + return jmxAgentMode; + } + + public boolean isSecure() { + init(); + if (isSecure==null) return false; + return isSecure; + } + + protected synchronized void init() { + if (isJmx!=null) + return; + + if (Boolean.FALSE.equals(entity.getConfig(USE_JMX))) { + isJmx = false; + return; + } + isJmx = true; + jmxAgentMode = entity.getConfig(JMX_AGENT_MODE); + if (jmxAgentMode==null) jmxAgentMode = JmxAgentModes.AUTODETECT; + + isSecure = entity.getConfig(JMX_SSL_ENABLED); + if (isSecure==null) isSecure = false; + + if (jmxAgentMode==JmxAgentModes.AUTODETECT) { + if (isSecure()) { + jmxAgentMode = JmxAgentModes.JMXMP; + } else { + jmxAgentMode = JmxAgentModes.JMXMP_AND_RMI; + if (!ResourceUtils.create(this).doesUrlExist(getJmxAgentJarUrl())) { + // can happen e.g. if eclipse build + log.warn("JMX agent JAR not found ("+getJmxAgentJarUrl()+") when auto-detecting JMX settings for "+entity+"; " + + "likely cause is an incomplete build (e.g. from Eclipse; run a maven build then retry in the IDE); "+ + "reverting to NONE (use built-in Java JMX support, which will not go through firewalls)"); + jmxAgentMode = JmxAgentModes.NONE; + } + } + + ((EntityLocal)entity).setConfig(JMX_AGENT_MODE, jmxAgentMode); + } + + if (isSecure && jmxAgentMode!=JmxAgentModes.JMXMP) { + String msg = "JMX SSL is specified, but it requires JMXMP which is disabled, when configuring "+entity; + log.warn(msg); + throw new IllegalStateException(msg); + } + } + + public void setJmxUrl() { + ((EntityInternal)entity).setAttribute(JMX_URL, getJmxUrl()); + } + + public String getJmxUrl() { + init(); + + HostAndPort jmx = BrooklynAccessUtils.getBrooklynAccessibleAddress(entity, entity.getAttribute(JMX_PORT)); + + if (EnumSet.of(JmxAgentModes.JMXMP, JmxAgentModes.JMXMP_AND_RMI).contains(getJmxAgentMode())) { + return JmxHelper.toJmxmpUrl(jmx.getHostText(), jmx.getPort()); + } else { + if (getJmxAgentMode() == JmxAgentModes.NONE) { + fixPortsForModeNone(); + } + // this will work for agent or agentless + HostAndPort rmi = BrooklynAccessUtils.getBrooklynAccessibleAddress(entity, entity.getAttribute(RMI_REGISTRY_PORT)); + return JmxHelper.toRmiJmxUrl(jmx.getHostText(), jmx.getPort(), rmi.getPort(), + entity.getAttribute(JMX_CONTEXT)); + } + } + + /** mode NONE cannot set a JMX (RMI server) port; it needs an RMI registry port, + * then gets redirected to an anonymous RMI server port; + * both the hostname and the anonymous port must be accessible to use this mode + * (hence the use of the other agents in most cases) */ + protected int fixPortsForModeNone() { + assert getJmxAgentMode()==JmxAgentModes.NONE; + Integer jmxRemotePort = getEntity().getAttribute(JMX_PORT); + Integer rmiRegistryPort = getEntity().getAttribute(RMI_REGISTRY_PORT); + if (rmiRegistryPort!=null && rmiRegistryPort>0) { + if (jmxRemotePort==null || jmxRemotePort!=rmiRegistryPort) { + if (jmxRemotePort!=null && jmxRemotePort>0) { + // ignore RMI registry port when mode 'none' is set -- set same as JMX port here + // (bit irritating, but JMX_PORT will be ignored in this mode) + log.warn("Ignoring JMX_PORT "+jmxRemotePort+" when configuring agentless JMX on "+getEntity()+"; will use RMI_REGISTRY_PORT "+rmiRegistryPort); + } + jmxRemotePort = rmiRegistryPort; + ((EntityLocal)getEntity()).setAttribute(JMX_PORT, jmxRemotePort); + } + } else { + if (jmxRemotePort==null || jmxRemotePort<=0) { + throw new IllegalStateException("Invalid JMX_PORT "+jmxRemotePort+" and RMI_REGISTRY_PORT "+rmiRegistryPort+" when configuring JMX "+getJmxAgentMode()+" on "+getEntity()); + } + ((EntityLocal)getEntity()).setAttribute(RMI_REGISTRY_PORT, jmxRemotePort); + } + return jmxRemotePort; + } + + public List<String> getJmxJavaConfigOptions() { + if (EnumSet.<JmxAgentModes>of(JmxAgentModes.NONE, JmxAgentModes.JMX_RMI).contains(getJmxAgentMode())) { + return MutableList.of(); + } else { + return MutableList.of(String.format("-javaagent:%s", getJmxAgentJarDestinationFilePath())); + } + } + + public String getJmxAgentJarDestinationFilePath() { + // cache the local path so we continue to work post-rebind to a different version + String result = getEntity().getAttribute(JMX_AGENT_LOCAL_PATH); + if (Strings.isNonBlank(result)) return result; + result = getJmxAgentJarDestinationFilePathDefault(); + ((EntityInternal)getEntity()).setAttribute(JMX_AGENT_LOCAL_PATH, result); + return result; + } + + public String getJmxAgentJarDestinationFilePathDefault() { + return Urls.mergePaths(getRunDir(), getJmxAgentJarBasename()); + } + + @Nullable public MavenArtifact getJmxAgentJarMavenArtifact() { + switch (getJmxAgentMode()) { + case JMXMP: + case JMXMP_AND_RMI: + MavenArtifact result = BrooklynMavenArtifacts.artifact(null, "brooklyn-jmxmp-agent", "jar", "with-dependencies"); + // the "with-dependencies" variant is needed; however the filename then has the classifier segment _replaced_ by "shaded" when this filename is created + result.setCustomFileNameAfterArtifactMarker("shaded"); + result.setClassifierFileNameMarker(""); + return result; + case JMX_RMI_CUSTOM_AGENT: + return BrooklynMavenArtifacts.jar("brooklyn-jmxrmi-agent"); + default: + return null; + } + } + + /** @deprecated since 0.6.0; use {@link #getJmxAgentJarMavenArtifact()} */ + @Deprecated + public String getJmxAgentJarBasename() { + MavenArtifact artifact = getJmxAgentJarMavenArtifact(); + if (artifact==null) + throw new IllegalStateException("Either JMX is not enabled or there is an error in the configuration (JMX mode "+getJmxAgentMode()+" does not support agent JAR)"); + return artifact.getFilename(); + } + + /** returns URL for accessing the java agent, throwing if not applicable; + * prefers on classpath where it should be, but will fall back to taking from maven hosted + * (known problem in Eclipse where JARs are not always copied) + */ + public String getJmxAgentJarUrl() { + MavenArtifact artifact = getJmxAgentJarMavenArtifact(); + if (artifact==null) + throw new IllegalStateException("Either JMX is not enabled or there is an error in the configuration (JMX mode "+getJmxAgentMode()+" does not support agent JAR)"); + String jar = "classpath://" + artifact.getFilename(); + if (ResourceUtils.create(this).doesUrlExist(jar)) + return jar; + + String result = MavenRetriever.localUrl(artifact); + if (warnedAboutNotOnClasspath) { + log.debug("JMX JAR for "+artifact+" is not on the classpath; taking from "+result); + } else { + log.warn("JMX JAR for "+artifact+" is not on the classpath; taking from "+result+" (subsequent similar messages will be logged at debug)"); + warnedAboutNotOnClasspath = true; + } + return result; + } + + /** applies _some_ of the common settings needed to connect via JMX */ + public void applyJmxJavaSystemProperties(MutableMap.Builder<String,Object> result) { + if (!isJmx()) return ; + + Integer jmxPort = Preconditions.checkNotNull(entity.getAttribute(JMX_PORT), "jmx port must not be null for %s", entity); + HostAndPort jmx = BrooklynAccessUtils.getBrooklynAccessibleAddress(entity, jmxPort); + Integer jmxRemotePort = getEntity().getAttribute(JMX_PORT); + String hostName = jmx.getHostText(); + + result.put("com.sun.management.jmxremote", null); + result.put("java.rmi.server.hostname", hostName); + + switch (getJmxAgentMode()) { + case JMXMP_AND_RMI: + Integer rmiRegistryPort = Preconditions.checkNotNull(entity.getAttribute(UsesJmx.RMI_REGISTRY_PORT), "registry port (config val %s)", entity.getConfig(UsesJmx.RMI_REGISTRY_PORT)); + result.put(JmxmpAgent.RMI_REGISTRY_PORT_PROPERTY, rmiRegistryPort); + case JMXMP: + if (jmxRemotePort==null || jmxRemotePort<=0) + throw new IllegalStateException("Unsupported JMX port "+jmxRemotePort+" - when applying system properties ("+getJmxAgentMode()+" / "+getEntity()+")"); + result.put(JmxmpAgent.JMXMP_PORT_PROPERTY, jmxRemotePort); + // with JMXMP don't try to tell it the hostname -- it isn't needed for JMXMP, and if specified + // it will break if the hostname we see is not known at the server, e.g. a forwarding public IP + result.remove("java.rmi.server.hostname"); + break; + case JMX_RMI_CUSTOM_AGENT: + if (jmxRemotePort==null || jmxRemotePort<=0) + throw new IllegalStateException("Unsupported JMX port "+jmxRemotePort+" - when applying system properties ("+getJmxAgentMode()+" / "+getEntity()+")"); + result.put(JmxRmiAgent.RMI_REGISTRY_PORT_PROPERTY, Preconditions.checkNotNull(entity.getAttribute(UsesJmx.RMI_REGISTRY_PORT), "registry port")); + result.put(JmxRmiAgent.JMX_SERVER_PORT_PROPERTY, jmxRemotePort); + break; + case NONE: + jmxRemotePort = fixPortsForModeNone(); + case JMX_RMI: + result.put("com.sun.management.jmxremote.port", jmxRemotePort); + result.put("java.rmi.server.useLocalHostname", "true"); + break; + default: + throw new IllegalStateException("Unsupported JMX mode - when applying system properties ("+getJmxAgentMode()+" / "+getEntity()+")"); + } + + if (isSecure()) { + // set values true, and apply keys pointing to keystore / truststore + getJmxSslSupport().applyAgentJmxJavaSystemProperties(result); + } else { + result. + put("com.sun.management.jmxremote.ssl", false). + put("com.sun.management.jmxremote.authenticate", false); + } + } + + /** installs files needed for JMX, to the runDir given in constructor, assuming the runDir has been created */ + public void install() { + if (EnumSet.of(JmxAgentModes.JMXMP_AND_RMI, JmxAgentModes.JMXMP, JmxAgentModes.JMX_RMI_CUSTOM_AGENT).contains(getJmxAgentMode())) { + Tasks.setBlockingDetails("Copying JMX agent jar to server."); + try { + getMachine().get().copyTo(ResourceUtils.create(this).getResourceFromUrl( + getJmxAgentJarUrl()), getJmxAgentJarDestinationFilePath()); + } finally { + Tasks.resetBlockingDetails(); + } + } + if (isSecure()) { + getJmxSslSupport().install(); + } + } + + protected JmxmpSslSupport getJmxSslSupport() { + return new JmxmpSslSupport(this); + } + + /** sets JMR_RMI_CUSTOM_AGENT as the connection mode for the indicated apps. + * <p> + * TODO callers of this method have RMI dependencies in the actual app; + * we should look at removing them, so that those pieces of software can run behind + * forwarding public IP's and over SSL (both reasons JMXMP is preferred by us!) + */ + public void recommendJmxRmiCustomAgent() { + // set JMX_RMI because the registry is needed (i think) + Maybe<Object> jmx = entity.getConfigRaw(UsesJmx.JMX_AGENT_MODE, true); + if (!jmx.isPresentAndNonNull()) { + setConfig(UsesJmx.JMX_AGENT_MODE, JmxAgentModes.JMX_RMI_CUSTOM_AGENT); + } else if (jmx.get()!=JmxAgentModes.JMX_RMI_CUSTOM_AGENT) { + log.warn("Entity "+entity+" may not function unless running JMX_RMI_CUSTOM_AGENT mode (asked to use "+jmx.get()+")"); + } + } + +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/main/java/org/apache/brooklyn/entity/java/JmxmpSslSupport.java ---------------------------------------------------------------------- diff --git a/software/base/src/main/java/org/apache/brooklyn/entity/java/JmxmpSslSupport.java b/software/base/src/main/java/org/apache/brooklyn/entity/java/JmxmpSslSupport.java new file mode 100644 index 0000000..36d1773 --- /dev/null +++ b/software/base/src/main/java/org/apache/brooklyn/entity/java/JmxmpSslSupport.java @@ -0,0 +1,134 @@ +/* + * 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.brooklyn.entity.java; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.security.KeyPair; +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; + +import org.apache.brooklyn.util.collections.MutableMap.Builder; +import org.apache.brooklyn.util.core.crypto.FluentKeySigner; +import org.apache.brooklyn.util.core.crypto.SecureKeys; +import org.apache.brooklyn.util.core.task.Tasks; +import org.apache.brooklyn.util.exceptions.Exceptions; +import org.apache.brooklyn.util.jmx.jmxmp.JmxmpAgent; +import org.apache.brooklyn.util.net.Urls; + +import com.google.common.base.Preconditions; + +public class JmxmpSslSupport { + + final static String BROOKLYN_VERSION = "0.8.0-SNAPSHOT"; // BROOKLYN_VERSION (updated by script) + + private final JmxSupport jmxSupport; + + private KeyStore agentTrustStore; + private KeyStore agentKeyStore; + + public JmxmpSslSupport(JmxSupport jmxSupport) { + this.jmxSupport = Preconditions.checkNotNull(jmxSupport); + } + + public String getJmxSslKeyStoreFilePath() { + return Urls.mergePaths(jmxSupport.getRunDir(), "jmx-keystore"); + } + + public String getJmxSslTrustStoreFilePath() { + return Urls.mergePaths(jmxSupport.getRunDir(), "jmx-truststore"); + } + + public void applyAgentJmxJavaSystemProperties(Builder<String, Object> result) { + result. + put(JmxmpAgent.USE_SSL_PROPERTY, true). + put(JmxmpAgent.AUTHENTICATE_CLIENTS_PROPERTY, true). + // the option below wants a jmxremote.password file; we use certs (above) to authenticate + put("com.sun.management.jmxremote.authenticate", false); + + result. + put(JmxmpAgent.JMXMP_KEYSTORE_FILE_PROPERTY, getJmxSslKeyStoreFilePath()). + put(JmxmpAgent.JMXMP_TRUSTSTORE_FILE_PROPERTY, getJmxSslTrustStoreFilePath()); + } + + public FluentKeySigner getBrooklynRootSigner() { + // TODO use brooklyn root CA keys etc + return new FluentKeySigner("brooklyn-root"); + } + + /** builds remote keystores, stores config keys/certs, and copies necessary files across */ + public void install() { + try { + // build truststore and keystore + FluentKeySigner signer = getBrooklynRootSigner(); + KeyPair jmxAgentKey = SecureKeys.newKeyPair(); + X509Certificate jmxAgentCert = signer.newCertificateFor("jmxmp-agent", jmxAgentKey); + + agentKeyStore = SecureKeys.newKeyStore(); + agentKeyStore.setKeyEntry("jmxmp-agent", jmxAgentKey.getPrivate(), + // TODO jmx.ssl.agent.keyPassword + "".toCharArray(), + new Certificate[] { jmxAgentCert }); + ByteArrayOutputStream agentKeyStoreBytes = new ByteArrayOutputStream(); + agentKeyStore.store(agentKeyStoreBytes, + // TODO jmx.ssl.agent.keyStorePassword + "".toCharArray()); + + agentTrustStore = SecureKeys.newKeyStore(); + agentTrustStore.setCertificateEntry("brooklyn", getJmxAccessCert()); + ByteArrayOutputStream agentTrustStoreBytes = new ByteArrayOutputStream(); + agentTrustStore.store(agentTrustStoreBytes, "".toCharArray()); + + // install the truststore and keystore and rely on JmxSupport to install the agent + Tasks.setBlockingDetails("Copying keystore and truststore to the server."); + try { + jmxSupport.getMachine().get().copyTo(new ByteArrayInputStream(agentKeyStoreBytes.toByteArray()), getJmxSslKeyStoreFilePath()); + jmxSupport.getMachine().get().copyTo(new ByteArrayInputStream(agentTrustStoreBytes.toByteArray()), getJmxSslTrustStoreFilePath()); + } finally { + Tasks.resetBlockingDetails(); + } + + } catch (Exception e) { + throw Exceptions.propagate(e); + } + } + + public synchronized Certificate getJmxAccessCert() { + Certificate cert = jmxSupport.getConfig(UsesJmx.JMX_SSL_ACCESS_CERT); + if (cert!=null) return cert; + // TODO load from keyStoreUrl + KeyPair jmxAccessKey = SecureKeys.newKeyPair(); + X509Certificate jmxAccessCert = getBrooklynRootSigner().newCertificateFor("brooklyn-jmx-access", jmxAccessKey); + + jmxSupport.setConfig(UsesJmx.JMX_SSL_ACCESS_CERT, jmxAccessCert); + jmxSupport.setConfig(UsesJmx.JMX_SSL_ACCESS_KEY, jmxAccessKey.getPrivate()); + + return jmxAccessCert; + } + + public synchronized PrivateKey getJmxAccessKey() { + PrivateKey key = jmxSupport.getConfig(UsesJmx.JMX_SSL_ACCESS_KEY); + if (key!=null) return key; + getJmxAccessCert(); + return jmxSupport.getConfig(UsesJmx.JMX_SSL_ACCESS_KEY); + } + +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/main/java/org/apache/brooklyn/entity/java/UsesJava.java ---------------------------------------------------------------------- diff --git a/software/base/src/main/java/org/apache/brooklyn/entity/java/UsesJava.java b/software/base/src/main/java/org/apache/brooklyn/entity/java/UsesJava.java new file mode 100644 index 0000000..1f80eda --- /dev/null +++ b/software/base/src/main/java/org/apache/brooklyn/entity/java/UsesJava.java @@ -0,0 +1,68 @@ +/* + * 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.brooklyn.entity.java; + +import org.apache.brooklyn.config.ConfigKey; +import org.apache.brooklyn.core.config.ConfigKeys; +import org.apache.brooklyn.core.config.MapConfigKey; +import org.apache.brooklyn.core.config.SetConfigKey; +import org.apache.brooklyn.util.core.flags.SetFromFlag; + +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Maps; + +public interface UsesJava { + + /** system properties (-D) to append to JAVA_OPTS; normally accessed through {@link JavaEntityMethods#javaSysProp(String)} */ + @SetFromFlag("javaSysProps") + public static final MapConfigKey<String> JAVA_SYSPROPS = new MapConfigKey<String>(String.class, + "java.sysprops", "Java command line system properties", Maps.<String,String>newLinkedHashMap()); + + /** + * Used to set java options. These options are prepended to the defaults. + * They can also be used to override defaults. The rules for overrides are: + * <ul> + * <li>If contains a mutually exclusive setting, then the others are removed. Those supported are: + * <ul> + * <li>"-client" and "-server" + * </ul> + * <li>If value has a well-known prefix indicating it's a key-value pair. Those supported are: + * <ul> + * <li>"-Xmx" + * <li>"-Xms" + * <li>"-Xss" + * </ul> + * <li>If value contains "=" then see if there's a default that matches the section up to the "=". + * If there is, then remove the original and just include this. + * e.g. "-XX:MaxPermSize=512m" could be overridden in this way. + * </ul> + */ + @SetFromFlag("javaOpts") + public static final SetConfigKey<String> JAVA_OPTS = new SetConfigKey<String>(String.class, + "java.opts", "Java command line options", ImmutableSet.<String>of()); + + public static final ConfigKey<Boolean> CHECK_JAVA_HOSTNAME_BUG = ConfigKeys.newBooleanConfigKey( + "java.check.hostname.bug", "Check whether hostname is too long and will likely crash Java" + + "due to bug 7089443", true); + + @SetFromFlag("javaVersionRequired") + ConfigKey<String> JAVA_VERSION_REQUIRED = ConfigKeys.newStringConfigKey("java.version.required", "Java version required", "1.7"); + + +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/main/java/org/apache/brooklyn/entity/java/UsesJavaMXBeans.java ---------------------------------------------------------------------- diff --git a/software/base/src/main/java/org/apache/brooklyn/entity/java/UsesJavaMXBeans.java b/software/base/src/main/java/org/apache/brooklyn/entity/java/UsesJavaMXBeans.java new file mode 100644 index 0000000..4f2d20b --- /dev/null +++ b/software/base/src/main/java/org/apache/brooklyn/entity/java/UsesJavaMXBeans.java @@ -0,0 +1,77 @@ +/* + * 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.brooklyn.entity.java; + +import java.util.Map; + +import org.apache.brooklyn.api.sensor.AttributeSensor; +import org.apache.brooklyn.config.ConfigKey; +import org.apache.brooklyn.core.config.ConfigKeys; +import org.apache.brooklyn.sensor.core.BasicAttributeSensor; +import org.apache.brooklyn.sensor.core.Sensors; +import org.apache.brooklyn.util.core.flags.SetFromFlag; + +public interface UsesJavaMXBeans { + + @SetFromFlag("mxbeanStatsEnabled") + ConfigKey<Boolean> MXBEAN_STATS_ENABLED = + ConfigKeys.newBooleanConfigKey("java.metrics.mxbeanStatsEnabled", "Enables collection of JVM stats from the MXBeans, such as memory and thread usage (default is true)", true); + + AttributeSensor<Long> USED_HEAP_MEMORY = + Sensors.newLongSensor("java.metrics.heap.used", "Current heap size (bytes)"); + AttributeSensor<Long> INIT_HEAP_MEMORY = + Sensors.newLongSensor("java.metrics.heap.init", "Initial heap size (bytes)"); + AttributeSensor<Long> COMMITTED_HEAP_MEMORY = + Sensors.newLongSensor("java.metrics.heap.committed", "Commited heap size (bytes)"); + AttributeSensor<Long> MAX_HEAP_MEMORY = + Sensors.newLongSensor("java.metrics.heap.max", "Max heap size (bytes)"); + AttributeSensor<Long> NON_HEAP_MEMORY_USAGE = + Sensors.newLongSensor("java.metrics.nonheap.used", "Current non-heap size (bytes)"); + AttributeSensor<Integer> CURRENT_THREAD_COUNT = + Sensors.newIntegerSensor( "java.metrics.threads.current", "Current number of threads"); + AttributeSensor<Integer> PEAK_THREAD_COUNT = + Sensors.newIntegerSensor("java.metrics.threads.max", "Peak number of threads"); + + // runtime system attributes + AttributeSensor<Long> START_TIME = + Sensors.newLongSensor("java.metrics.starttime", "Start time of Java process (UTC)"); + AttributeSensor<Long> UP_TIME = + Sensors.newLongSensor("java.metrics.uptime", "Uptime of Java process (millis, elapsed since start)"); + + //operating system attributes + AttributeSensor<Double> PROCESS_CPU_TIME = Sensors.newDoubleSensor( + "java.metrics.processCpuTime.total", "Process CPU time (total millis since start)"); + AttributeSensor<Double> PROCESS_CPU_TIME_FRACTION_LAST = Sensors.newDoubleSensor( + "java.metrics.processCpuTime.fraction.last", "Fraction of CPU time used, reported by JVM (percentage, last datapoint)"); + AttributeSensor<Double> PROCESS_CPU_TIME_FRACTION_IN_WINDOW = Sensors.newDoubleSensor( + "java.metrics.processCpuTime.fraction.windowed", "Fraction of CPU time used, reported by JVM (percentage, over time window)"); + + AttributeSensor<Integer> AVAILABLE_PROCESSORS = + Sensors.newIntegerSensor("java.metrics.processors.available", "number of processors available to the Java virtual machine"); + AttributeSensor<Double> SYSTEM_LOAD_AVERAGE + = Sensors.newDoubleSensor("java.metrics.systemload.average", "average system load"); + AttributeSensor<Long> TOTAL_PHYSICAL_MEMORY_SIZE = + Sensors.newLongSensor("java.metrics.physicalmemory.total", "The physical memory available to the operating system"); + AttributeSensor<Long> FREE_PHYSICAL_MEMORY_SIZE = + Sensors.newLongSensor("java.metrics.physicalmemory.free", "The free memory available to the operating system"); + + // GC attributes + AttributeSensor<Map> GARBAGE_COLLECTION_TIME = new BasicAttributeSensor<Map>(Map.class, "java.metrics.gc.time", "garbage collection time"); + +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/main/java/org/apache/brooklyn/entity/java/UsesJmx.java ---------------------------------------------------------------------- diff --git a/software/base/src/main/java/org/apache/brooklyn/entity/java/UsesJmx.java b/software/base/src/main/java/org/apache/brooklyn/entity/java/UsesJmx.java new file mode 100644 index 0000000..4831826 --- /dev/null +++ b/software/base/src/main/java/org/apache/brooklyn/entity/java/UsesJmx.java @@ -0,0 +1,190 @@ +/* + * 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.brooklyn.entity.java; + +import java.security.PrivateKey; +import java.security.cert.Certificate; + +import org.apache.brooklyn.api.entity.Entity; +import org.apache.brooklyn.api.location.PortRange; +import org.apache.brooklyn.api.sensor.AttributeSensor; +import org.apache.brooklyn.config.ConfigKey; +import org.apache.brooklyn.core.config.BasicConfigKey; +import org.apache.brooklyn.core.config.ConfigKeys; +import org.apache.brooklyn.location.basic.PortRanges; +import org.apache.brooklyn.sensor.core.AttributeSensorAndConfigKey; +import org.apache.brooklyn.sensor.core.BasicAttributeSensorAndConfigKey; +import org.apache.brooklyn.sensor.core.PortAttributeSensorAndConfigKey; +import org.apache.brooklyn.util.core.flags.SetFromFlag; + +public interface UsesJmx extends UsesJava { + + public static final int DEFAULT_JMX_PORT = 1099; // RMI port? + + @SetFromFlag("useJmx") + ConfigKey<Boolean> USE_JMX = ConfigKeys.newConfigKey("jmx.enabled", "JMX enabled", Boolean.TRUE); + + /** Chosen by Java itself by default, setting this will only have any effect if using an agent. */ + @SetFromFlag("jmxPort") + PortAttributeSensorAndConfigKey JMX_PORT = new PortAttributeSensorAndConfigKey( + "jmx.direct.port", "JMX direct/private port (e.g. JMX RMI server port, or JMXMP port, but not RMI registry port)", PortRanges.fromString("31001+")); + + // Default is deliberately null for this unused config; if we used "31001+" then we'd potentially give this sensor + // the value 31001 and jmx.direct.port the value 31002. See https://issues.apache.org/jira/browse/BROOKLYN-98 + /** @deprecated since 0.7.0, kept for rebinding with the anonymous class; code should only ever use {@link #JMX_PORT} */ @Deprecated + PortAttributeSensorAndConfigKey JMX_PORT_LEGACY = new PortAttributeSensorAndConfigKey( + "jmx.direct.port.legacy.NOT_USED", "Legacy definition JMX direct/private port (e.g. JMX RMI server port, or JMXMP port, but not RMI registry port)", null) { + private static final long serialVersionUID = 3846846080809179437L; + @Override protected Integer convertConfigToSensor(PortRange value, Entity entity) { + // TODO when using JmxAgentModes.NONE we should *not* convert, but leave it null + // (e.g. to prevent a warning in e.g. ActiveMQIntegrationTest) + // [there was - previously - a note about needing to move these keys to UsesJmx, + // that has been done, so not sure if there is anything more needed or if we can just + // check here entity.getConfig(JMX_AGENT_MODE) ... needs testing of course] + return super.convertConfigToSensor(value, entity); + } + }; + + /** Well-known port used by Java itself to start the RMI registry where JMX private port can be discovered, ignored if using JMXMP agent. */ + @SetFromFlag("rmiRegistryPort") + PortAttributeSensorAndConfigKey RMI_REGISTRY_PORT = ConfigKeys.newPortSensorAndConfigKey( + "rmi.registry.port", "RMI registry port, used for discovering JMX (private) port", PortRanges.fromString("1099,19099+")); + + @SetFromFlag("jmxContext") + AttributeSensorAndConfigKey<String, String> JMX_CONTEXT = ConfigKeys.newStringSensorAndConfigKey("jmx.context", "JMX context path", "jmxrmi"); + + AttributeSensor<String> JMX_URL = new BasicAttributeSensorAndConfigKey<String>( + String.class, "jmx.service.url", "The URL for connecting to the MBean Server"); + + /** Forces JMX to be secured, using JMXMP so it gets through firewalls <em>and</em> SSL/TLS. */ + @SetFromFlag("jmxSecure") + ConfigKey<Boolean> JMX_SSL_ENABLED = ConfigKeys.newBooleanConfigKey("jmx.ssl.enabled", "JMX over JMXMP enabled with SSL/TLS", Boolean.FALSE); + + enum JmxAgentModes { + /** Auto-detect the agent to use based on location. Prefer {@link #JMXMP} except at localhost which uses {@link #JMX_RMI_CUSTOM_AGENT}. */ + AUTODETECT, + + /** JMXMP which permits firewall access through a single port {@link UsesJmx#JMX_PORT}. */ + JMXMP, + + /** Start {@link #JMXMP} along with an RMI Registry on {@link UsesJmx#RMI_REGISTRY_PORT}, redirecting to an anonymous high-numbered port as the RMI server. */ + JMXMP_AND_RMI, + + /** JMX over RMI custom agent which permits access through a known {@link UsesJmx#RMI_REGISTRY_PORT}, redirected to a known {@link UsesJmx#JMX_PORT}. + * Both ports must be opened on the firewall, and the same hostname resolvable on the target machine and by the client */ + JMX_RMI_CUSTOM_AGENT, + + /** As with {@link UsesJmx#JMX_RMI_CUSTOM_AGENT} but no custom agent requred, entity must handle pots correctly. */ + JMX_RMI, + + /** Do not install a JMX agent. Use the default {@link UsesJmx#RMI_REGISTRY_PORT}, redirected to an unknown port for JMX. */ + NONE + } + + @SetFromFlag("jmxAgentMode") + ConfigKey<JmxAgentModes> JMX_AGENT_MODE = ConfigKeys.newConfigKey("jmx.agent.mode", + "What type of JMX agent to use; defaults to null (autodetect) which means " + + "JMXMP_AND_RMI allowing firewall access through a single port as well as local access supporting jconsole " + + "(unless JMX_SSL_ENABLED is set, in which case it is JMXMP only)", + JmxAgentModes.AUTODETECT); + + /* Currently these are only used to connect, so only applies where systems set this up themselves. */ + AttributeSensorAndConfigKey<String, String> JMX_USER = ConfigKeys.newStringSensorAndConfigKey("jmx.user", "JMX username"); + AttributeSensorAndConfigKey<String, String> JMX_PASSWORD = ConfigKeys.newStringSensorAndConfigKey("jmx.password", "JMX password"); + + AttributeSensorAndConfigKey<String, String> JMX_AGENT_LOCAL_PATH = ConfigKeys.newStringSensorAndConfigKey("jmx.agent.local.path", "Path to JMX driver on the local machine"); + + /* + * Synopsis of how the keys work for JMX_SSL: + * + * BROOKLYN + * * brooklyn ROOT key + cert -> + * used to identify things brooklyn has signed, ie to confirm their identity + * signs all certs created by brooklyn + * (created per entity if not specified as input) + * * brooklyn JMX ACCESS key + cert -> + * used to authenticate brooklyn to remote JMX agent + * typically, but not necessarily, signed by ROOT cert + * (typically created per entity, unless specified; + * global would probably be fine but more work; + * however it is important that this _not_ sign agents keys, + * to prevent agents from accessing other agents) + * + * AGENT (e.g. JMX server in each managed java process) + * * gets AGENT key + cert -> + * signed by brooklyn ROOT, used to authenticate itself to brooklyn + * (brooklyn trusts this; does not need to remember this) + * * trusts only the relevant brooklyn JMX ACCESS key (its truststore contains that cert) + */ + + /* TODO brooklyn ROOT key + * + public static final ConfigKey<String> BROOKLYN_SSL_ROOT_KEYSTORE_URL = new BasicConfigKey<String>( + String.class, "brooklyn.ssl.root.keyStoreUrl", "URL to keystore Brooklyn should use as root private key and certificate-signing authority", null); + + public static final ConfigKey<String> BROOKLYN_SSL_ROOT_KEY_DATA = new BasicConfigKey<String>( + String.class, "brooklyn.ssl.root.key", "root private key (RSA string format), used to sign managed servers", null); + public static final ConfigKey<String> BROOKLYN_SSL_ROOT_CERT_DATA = new BasicConfigKey<String>( + String.class, "brooklyn.ssl.root.cert", "certificate for root private key (RSA string format)", null); + + * brooklyn.ssl.root.keyStorePassword + * brooklyn.ssl.root.keyAlias (if null, looks for one called 'brooklyn', otherwise takes the first key) + * brooklyn.ssl.root.keyPassword + */ + + public static final ConfigKey<PrivateKey> JMX_SSL_ACCESS_KEY = new BasicConfigKey<PrivateKey>( + PrivateKey.class, "jmx.ssl.access.key", "key used to access a JMX agent (typically per entity, embedded in the managed JVM)", null); + public static final ConfigKey<Certificate> JMX_SSL_ACCESS_CERT = new BasicConfigKey<Certificate>( + Certificate.class, "jmx.ssl.access.cert", "certificate of key used to access a JMX agent", null); + + /* TODO specify a keystore from which to get the access key + * (above keys are set programmatically, typically _not_ by the user ... keystore would be the way to do that) + * + * jmx.ssl.access.keyStoreUrl (optional) + * jmx.ssl.access.keyStorePassword (optional) + * jmx.ssl.access.keyAlias (optional) + */ + + /* could allow user to specify additional certs for JMX agents which should be trusted + * + * jmx.ssl.access.trustStoreUrl + */ + + /* optionally: could allow JMX agent to trust additional accessers, + * and/or use known keys in the case that other accessers might want to authenticate the JMX server + * + * NB currently agent keys are not stored in brooklyn... no reason to as + * (a) currently we trust jmx agents; and (b) for agent-auth we should simply sign keys; + * either way, seems fine for brooklyn to throw them away once they are installed on the remote machine) + * + * jmx.ssl.agent.keyStoreUrl + * jmx.ssl.agent.keyStorePassword + * jmx.ssl.agent.keyAlias + * jmx.ssl.agent.keyPassword + * + * jmx.ssl.agent.trustStoreUrl + */ + + /* optionally: this could be set to disallow attaching to JMX through the attach mechanism + * (but this option is generally not considered needed, as JVM attachment is + * already restricted to localhost and to the the user running the process) + * + * -XX:+DisableAttachMechanism + */ +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/main/java/org/apache/brooklyn/entity/java/VanillaJavaApp.java ---------------------------------------------------------------------- diff --git a/software/base/src/main/java/org/apache/brooklyn/entity/java/VanillaJavaApp.java b/software/base/src/main/java/org/apache/brooklyn/entity/java/VanillaJavaApp.java new file mode 100644 index 0000000..9319406 --- /dev/null +++ b/software/base/src/main/java/org/apache/brooklyn/entity/java/VanillaJavaApp.java @@ -0,0 +1,77 @@ +/* + * 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.brooklyn.entity.java; + +import java.util.List; +import java.util.Map; + +import org.apache.brooklyn.api.entity.ImplementedBy; +import org.apache.brooklyn.api.sensor.AttributeSensor; +import org.apache.brooklyn.config.ConfigKey; +import org.apache.brooklyn.core.config.ConfigKeys; +import org.apache.brooklyn.entity.software.base.SoftwareProcess; +import org.apache.brooklyn.sensor.core.Sensors; +import org.apache.brooklyn.util.collections.MutableList; +import org.apache.brooklyn.util.core.flags.SetFromFlag; + +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; + +@ImplementedBy(VanillaJavaAppImpl.class) +public interface VanillaJavaApp extends SoftwareProcess, UsesJava, UsesJmx, UsesJavaMXBeans { + + // TODO Make jmxPollPeriod @SetFromFlag easier to use: currently a confusion over long and TimeDuration, and + // no ability to set default value (can't just set field because config vals read/set in super-constructor :-( + + @SetFromFlag("args") + ConfigKey<List> ARGS = ConfigKeys.newConfigKey(List.class, + "vanillaJavaApp.args", "Arguments for launching the java app", Lists.newArrayList()); + + @SetFromFlag(value="main", nullable=false) + ConfigKey<String> MAIN_CLASS = ConfigKeys.newStringConfigKey("vanillaJavaApp.mainClass", "class to launch"); + + @SetFromFlag("classpath") + ConfigKey<List> CLASSPATH = ConfigKeys.newConfigKey(List.class, + "vanillaJavaApp.classpath", "classpath to use, as list of URL entries; " + + "these URLs are copied to lib/ and expanded in the case of tar/tgz/zip", + Lists.newArrayList()); + + AttributeSensor<List> CLASSPATH_FILES = Sensors.newSensor(List.class, + "vanillaJavaApp.classpathFiles", "classpath used, list of files"); + + @SetFromFlag("jvmXArgs") + ConfigKey<List> JVM_XARGS = ConfigKeys.newConfigKey(List.class, + "vanillaJavaApp.jvmXArgs", "JVM -X args for the java app (e.g. memory)", + MutableList.of("-Xms128m", "-Xmx512m", "-XX:MaxPermSize=512m")); + + @SetFromFlag("jvmDefines") + ConfigKey<Map> JVM_DEFINES = ConfigKeys.newConfigKey(Map.class, + "vanillaJavaApp.jvmDefines", "JVM system property definitions for the app", + Maps.newLinkedHashMap()); + + public String getMainClass(); + public List<String> getClasspath(); + public List<String> getClasspathFiles(); + public Map getJvmDefines(); + public List getJvmXArgs(); + public String getRunDir(); + + public void kill(); + +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/main/java/org/apache/brooklyn/entity/java/VanillaJavaAppDriver.java ---------------------------------------------------------------------- diff --git a/software/base/src/main/java/org/apache/brooklyn/entity/java/VanillaJavaAppDriver.java b/software/base/src/main/java/org/apache/brooklyn/entity/java/VanillaJavaAppDriver.java new file mode 100644 index 0000000..fd8c624 --- /dev/null +++ b/software/base/src/main/java/org/apache/brooklyn/entity/java/VanillaJavaAppDriver.java @@ -0,0 +1,26 @@ +/* + * 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.brooklyn.entity.java; + +/** + * The {@link JavaSoftwareProcessDriver} for a {@link VanillaJavaApp}. + */ +public interface VanillaJavaAppDriver extends JavaSoftwareProcessDriver { + +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/main/java/org/apache/brooklyn/entity/java/VanillaJavaAppImpl.java ---------------------------------------------------------------------- diff --git a/software/base/src/main/java/org/apache/brooklyn/entity/java/VanillaJavaAppImpl.java b/software/base/src/main/java/org/apache/brooklyn/entity/java/VanillaJavaAppImpl.java new file mode 100644 index 0000000..a81de86 --- /dev/null +++ b/software/base/src/main/java/org/apache/brooklyn/entity/java/VanillaJavaAppImpl.java @@ -0,0 +1,112 @@ +/* + * 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.brooklyn.entity.java; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import org.apache.brooklyn.api.entity.Entity; +import org.apache.brooklyn.entity.software.base.SoftwareProcessImpl; +import org.apache.brooklyn.sensor.feed.jmx.JmxFeed; +import org.apache.brooklyn.util.core.flags.SetFromFlag; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.annotations.VisibleForTesting; + +public class VanillaJavaAppImpl extends SoftwareProcessImpl implements VanillaJavaApp { + + static { + JavaAppUtils.init(); + } + + private static final Logger log = LoggerFactory.getLogger(VanillaJavaApp.class); + + @SetFromFlag + protected long jmxPollPeriod; + + protected JmxFeed jmxFeed; + + public VanillaJavaAppImpl() {} + + @VisibleForTesting + public VanillaJavaAppImpl(Map<?,?> properties, Entity parent) { + super(properties, parent); + } + + public String getMainClass() { return getConfig(MAIN_CLASS); } + public List<String> getClasspath() { return getConfig(CLASSPATH); } + public List<String> getClasspathFiles() { return getAttribute(CLASSPATH_FILES); } + public Map getJvmDefines() { return getConfig(JVM_DEFINES); } + public List getJvmXArgs() { return getConfig(JVM_XARGS); } + + public void addToClasspath(String url) { + List<String> cp = getConfig(CLASSPATH); + List<String> newCP = new ArrayList<String>(); + if (cp!=null) newCP.addAll(cp); + newCP.add(url); + setConfig(CLASSPATH, newCP); + } + + public void addToClasspath(Collection<String> urls) { + List<String> cp = getConfig(CLASSPATH); + List<String> newCP = new ArrayList<String>(); + if (cp!=null) newCP.addAll(cp); + newCP.addAll(urls); + setConfig(CLASSPATH, newCP); + } + + @Override + protected void connectSensors() { + super.connectSensors(); + + if (((VanillaJavaAppDriver) getDriver()).isJmxEnabled()) { + jmxPollPeriod = (jmxPollPeriod > 0) ? jmxPollPeriod : 3000; + jmxFeed = JavaAppUtils.connectMXBeanSensors(this, jmxPollPeriod); + } + + connectServiceUpIsRunning(); + } + + @Override + public void disconnectSensors() { + super.disconnectSensors(); + disconnectServiceUpIsRunning(); + if (jmxFeed != null) jmxFeed.stop(); + } + + @Override + public Class<? extends VanillaJavaAppDriver> getDriverInterface() { + return VanillaJavaAppDriver.class; + } + + public String getRunDir() { + // FIXME Make this an attribute; don't assume it hsa to be ssh? What uses this? + VanillaJavaAppSshDriver driver = (VanillaJavaAppSshDriver) getDriver(); + return (driver != null) ? driver.getRunDir() : null; + } + + @Override + public void kill() { + getDriver().kill(); + } + +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/main/java/org/apache/brooklyn/entity/java/VanillaJavaAppSshDriver.java ---------------------------------------------------------------------- diff --git a/software/base/src/main/java/org/apache/brooklyn/entity/java/VanillaJavaAppSshDriver.java b/software/base/src/main/java/org/apache/brooklyn/entity/java/VanillaJavaAppSshDriver.java new file mode 100644 index 0000000..011f4ff --- /dev/null +++ b/software/base/src/main/java/org/apache/brooklyn/entity/java/VanillaJavaAppSshDriver.java @@ -0,0 +1,211 @@ +/* + * 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.brooklyn.entity.java; + +import static java.lang.String.format; + +import java.io.File; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import javax.annotation.Nullable; + +import org.apache.brooklyn.entity.software.base.lifecycle.ScriptHelper; +import org.apache.brooklyn.location.basic.SshMachineLocation; +import org.apache.brooklyn.util.collections.MutableList; +import org.apache.brooklyn.util.collections.MutableMap; +import org.apache.brooklyn.util.core.file.ArchiveBuilder; +import org.apache.brooklyn.util.core.file.ArchiveUtils; +import org.apache.brooklyn.util.core.internal.ssh.SshTool; +import org.apache.brooklyn.util.core.task.Tasks; +import org.apache.brooklyn.util.exceptions.Exceptions; +import org.apache.brooklyn.util.net.Urls; +import org.apache.brooklyn.util.os.Os; +import org.apache.brooklyn.util.text.Strings; +import org.apache.brooklyn.util.text.StringEscapes.BashStringEscapes; + +import com.google.common.base.CharMatcher; +import com.google.common.base.Function; +import com.google.common.base.Joiner; +import com.google.common.base.Splitter; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; + +/** + * The SSH implementation of the {@link VanillaJavaAppDriver}. + */ +public class VanillaJavaAppSshDriver extends JavaSoftwareProcessSshDriver implements VanillaJavaAppDriver { + + // FIXME this should be a config, either on the entity or -- probably better -- + // an alternative / override timeout on the SshTool for file copy commands + final static int NUM_RETRIES_FOR_COPYING = 4; + + public VanillaJavaAppSshDriver(VanillaJavaAppImpl entity, SshMachineLocation machine) { + super(entity, machine); + } + + @Override + public VanillaJavaAppImpl getEntity() { + return (VanillaJavaAppImpl) super.getEntity(); + } + + @Override + protected String getLogFileLocation() { + return Os.mergePathsUnix(getRunDir(), "console"); + } + + @Override + public void install() { + newScript(INSTALLING).execute(); + } + + @Override + public void customize() { + newScript(CUSTOMIZING) + .body.append(format("mkdir -p %s/lib", getRunDir())) + .failOnNonZeroResultCode() + .execute(); + + SshMachineLocation machine = getMachine(); + VanillaJavaApp entity = getEntity(); + for (String entry : entity.getClasspath()) { + // If a local folder, then create archive from contents first + if (Urls.isDirectory(entry)) { + File jarFile = ArchiveBuilder.jar().addDirContentsAt(new File(entry), "").create(); + entry = jarFile.getAbsolutePath(); + } + + // Determine filename + String destFile = entry.contains("?") ? entry.substring(0, entry.indexOf('?')) : entry; + destFile = destFile.substring(destFile.lastIndexOf('/') + 1); + + ArchiveUtils.deploy(MutableMap.<String, Object>of(), entry, machine, getRunDir(), Os.mergePaths(getRunDir(), "lib"), destFile); + } + + ScriptHelper helper = newScript(CUSTOMIZING+"-classpath") + .body.append(String.format("ls -1 \"%s\"", Os.mergePaths(getRunDir(), "lib"))) + .gatherOutput(); + helper.setFlag(SshTool.PROP_NO_EXTRA_OUTPUT, true); + int result = helper.execute(); + if (result != 0) { + throw new IllegalStateException("Error listing classpath files: " + helper.getResultStderr()); + } + String stdout = helper.getResultStdout(); + + // Transform stdout into list of files in classpath + if (Strings.isBlank(stdout)) { + getEntity().setAttribute(VanillaJavaApp.CLASSPATH_FILES, ImmutableList.of(Os.mergePaths(getRunDir(), "lib"))); + } else { + // FIXME Cannot handle spaces in paths properly + Iterable<String> lines = Splitter.on(CharMatcher.BREAKING_WHITESPACE).omitEmptyStrings().trimResults().split(stdout); + Iterable<String> files = Iterables.transform(lines, new Function<String, String>() { + @Override + public String apply(@Nullable String input) { + return Os.mergePathsUnix(getRunDir(), "lib", input); + } + }); + getEntity().setAttribute(VanillaJavaApp.CLASSPATH_FILES, ImmutableList.copyOf(files)); + } + } + + public String getClasspath() { + @SuppressWarnings("unchecked") + List<String> files = getEntity().getAttribute(VanillaJavaApp.CLASSPATH_FILES); + if (files == null || files.isEmpty()) { + return null; + } else { + return Joiner.on(":").join(files); + } + } + + @Override + public void launch() { + String clazz = getEntity().getMainClass(); + String args = getArgs(); + + newScript(MutableMap.of(USE_PID_FILE, true), LAUNCHING) + .body.append( + format("echo \"launching: java $JAVA_OPTS %s %s\"", clazz, args), + format("java $JAVA_OPTS -cp \"%s\" %s %s >> %s/console 2>&1 </dev/null &", getClasspath(), clazz, args, getRunDir()) + ) + .execute(); + } + + public String getArgs(){ + List<Object> args = getEntity().getConfig(VanillaJavaApp.ARGS); + StringBuilder sb = new StringBuilder(); + Iterator<Object> it = args.iterator(); + while (it.hasNext()) { + Object argO = it.next(); + try { + String arg = Tasks.resolveValue(argO, String.class, getEntity().getExecutionContext()); + BashStringEscapes.assertValidForDoubleQuotingInBash(arg); + sb.append(format("\"%s\"",arg)); + } catch (Exception e) { + throw Exceptions.propagate(e); + } + if (it.hasNext()) { + sb.append(" "); + } + } + + return sb.toString(); + } + + @Override + public boolean isRunning() { + int result = newScript(MutableMap.of(USE_PID_FILE, true), CHECK_RUNNING).execute(); + return result == 0; + } + + @Override + public void stop() { + newScript(MutableMap.of(USE_PID_FILE, true), STOPPING).execute(); + } + + @Override + public void kill() { + newScript(MutableMap.of(USE_PID_FILE, true), KILLING).execute(); + } + + @Override + protected Map getCustomJavaSystemProperties() { + return MutableMap.builder() + .putAll(super.getCustomJavaSystemProperties()) + .putAll(getEntity().getJvmDefines()) + .build(); + } + + @Override + protected List<String> getCustomJavaConfigOptions() { + return MutableList.<String>builder() + .addAll(super.getCustomJavaConfigOptions()) + .addAll(getEntity().getJvmXArgs()) + .build(); + } + + @Override + public Map<String,String> getShellEnvironment() { + return MutableMap.<String,String>builder() + .putAll(super.getShellEnvironment()) + .putIfNotNull("CLASSPATH", getClasspath()) + .build(); + } +}
