| /* |
| * 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.entity.EntityLocal; |
| import org.apache.brooklyn.core.effector.EffectorTasks; |
| import org.apache.brooklyn.core.effector.ssh.SshEffectorTasks; |
| import org.apache.brooklyn.core.entity.Attributes; |
| import org.apache.brooklyn.core.entity.Entities; |
| import org.apache.brooklyn.core.entity.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.ssh.SshMachineLocation; |
| 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(); }}); |
| } |
| } |
| |
| } |